From 93f7ef50f6b5276d3a04a39fb1c351b33302ada5 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Fri, 26 Feb 2021 13:55:02 -0500 Subject: [PATCH 01/50] Issue #1980 - Introduce fhir-term-graph module Signed-off-by: John T.E. Timm --- .../ibm/fhir/config/FHIRConfiguration.java | 2 + .../resource/FHIRRegistryResource.java | 4 +- .../fhir/registry/util/FHIRRegistryUtil.java | 2 +- .../com/ibm/fhir/registry/util/Index.java | 6 +- fhir-server/pom.xml | 6 +- .../listener/FHIRServletContextListener.java | 29 +- .../registry/ServerRegistryResource.java | 6 +- fhir-term-graph/pom.xml | 62 ++++ .../ibm/fhir/term/graph/FHIRTermGraph.java | 27 ++ .../fhir/term/graph/FHIRTermGraphFactory.java | 28 ++ .../term/graph/impl/FHIRTermGraphImpl.java | 184 +++++++++++ .../graph/loader/SnomedTermGraphLoader.java | 295 ++++++++++++++++++ .../provider/GraphTermServiceProvider.java | 217 +++++++++++++ .../term/graph/util/FHIRTermGraphUtil.java | 51 +++ .../resources/conf/local-graph.properties | 12 + .../term/graph/test/FHIRTermGraphTest.java | 52 +++ .../test/GraphTermServiceProviderTest.java | 55 ++++ .../resources/conf/local-graph.properties | 5 + .../fhir/term/service/FHIRTermService.java | 98 +++--- .../provider/DefaultTermServiceProvider.java | 21 +- .../term/spi/FHIRTermServiceProvider.java | 39 +-- .../com/ibm/fhir/term/spi/LookupOutcome.java | 10 +- .../ibm/fhir/term/util/ValueSetSupport.java | 16 +- 23 files changed, 1123 insertions(+), 104 deletions(-) create mode 100644 fhir-term-graph/pom.xml create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraphFactory.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/SnomedTermGraphLoader.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/provider/GraphTermServiceProvider.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java create mode 100644 fhir-term-graph/src/main/resources/conf/local-graph.properties create mode 100644 fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java create mode 100644 fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java create mode 100644 fhir-term-graph/src/test/resources/conf/local-graph.properties diff --git a/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java b/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java index 216e4419b72..4c00f0851a0 100644 --- a/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java +++ b/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java @@ -33,6 +33,8 @@ public class FHIRConfiguration { public static final String PROPERTY_CHECK_REFERENCE_TYPES = "fhirServer/core/checkReferenceTypes"; public static final String PROPERTY_CONDITIONAL_DELETE_MAX_NUMBER = "fhirServer/core/conditionalDeleteMaxNumber"; public static final String PROPERTY_SERVER_REGISTRY_RESOURCE_PROVIDER_ENABLED = "fhirServer/core/serverRegistryResourceProviderEnabled"; + public static final String PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_ENABLED = "fhirServer/core/graphTermServiceProvider/enabled"; + public static final String PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_CONFIGURATION = "fhirServer/core/graphTermServiceProvider/configuration"; public static final String PROPERTY_CAPABILITY_STATEMENT_CACHE = "fhirServer/core/capabilityStatementCacheTimeout"; public static final String PROPERTY_EXTENDED_CODEABLE_CONCEPT_VALIDATION = "fhirServer/core/extendedCodeableConceptValidation"; public static final String PROPERTY_DISABLED_OPERATIONS = "fhirServer/core/disabledOperations"; 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 c0229dbdea3..79fddbf8760 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 @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2019, 2020 + * (C) Copyright IBM Corp. 2019, 2021 * * SPDX-License-Identifier: Apache-2.0 */ @@ -14,6 +14,8 @@ * An abstract base class that contains the metadata for a definitional resource (e.g. StructureDefinition) */ public abstract class FHIRRegistryResource implements Comparable { + public static final Version NO_VERSION = Version.from(""); + protected final Class resourceType; protected final String id; protected final String url; diff --git a/fhir-registry/src/main/java/com/ibm/fhir/registry/util/FHIRRegistryUtil.java b/fhir-registry/src/main/java/com/ibm/fhir/registry/util/FHIRRegistryUtil.java index c5647cb4837..cd05bd90f25 100644 --- a/fhir-registry/src/main/java/com/ibm/fhir/registry/util/FHIRRegistryUtil.java +++ b/fhir-registry/src/main/java/com/ibm/fhir/registry/util/FHIRRegistryUtil.java @@ -152,7 +152,7 @@ public static Collection getRegistryResources(String packa ModelSupport.getResourceType(entry.getResourceType()), entry.getId(), entry.getUrl(), - Version.from(entry.getVersion()), + (entry.getVersion() != null) ? Version.from(entry.getVersion()) : FHIRRegistryResource.NO_VERSION, entry.getKind(), entry.getType(), packageDirectory + "/" + entry.getFileName())); diff --git a/fhir-registry/src/main/java/com/ibm/fhir/registry/util/Index.java b/fhir-registry/src/main/java/com/ibm/fhir/registry/util/Index.java index 43b967effef..40f26004c80 100644 --- a/fhir-registry/src/main/java/com/ibm/fhir/registry/util/Index.java +++ b/fhir-registry/src/main/java/com/ibm/fhir/registry/util/Index.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */ @@ -237,7 +237,7 @@ private Entry( this.resourceType = Objects.requireNonNull(resourceType, "resourceType"); this.id = Objects.requireNonNull(id, "id"); this.url = Objects.requireNonNull(url, "url"); - this.version = Objects.requireNonNull(version, "version"); + this.version = version; this.kind = kind; this.type = type; } @@ -284,7 +284,7 @@ public static Entry entry(Resource resource) { String url = FHIRRegistryUtil.getUrl(resource); String version = FHIRRegistryUtil.getVersion(resource); - if (url == null || version == null) { + if (url == null) { return null; } diff --git a/fhir-server/pom.xml b/fhir-server/pom.xml index 8a38c685c61..1d4ff3b492f 100644 --- a/fhir-server/pom.xml +++ b/fhir-server/pom.xml @@ -52,7 +52,11 @@ fhir-persistence ${project.version} - + + ${project.groupId} + fhir-term-graph + ${project.version} + ${project.groupId} fhir-notification diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java b/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java index 8a6a6e044cf..b44bf944f5e 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2016, 2020 + * (C) Copyright IBM Corp. 2016, 2021 * * SPDX-License-Identifier: Apache-2.0 */ @@ -8,6 +8,8 @@ import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_CHECK_REFERENCE_TYPES; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_EXTENDED_CODEABLE_CONCEPT_VALIDATION; +import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_CONFIGURATION; +import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_ENABLED; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_JDBC_BOOTSTRAP_DB; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_KAFKA_CONNECTIONPROPS; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_KAFKA_ENABLED; @@ -25,7 +27,9 @@ import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_SERVER_REGISTRY_RESOURCE_PROVIDER_ENABLED; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_WEBSOCKET_ENABLED; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.logging.Level; import java.util.logging.Logger; @@ -39,6 +43,7 @@ import javax.transaction.UserTransaction; import javax.websocket.server.ServerContainer; +import org.apache.commons.configuration.MapConfiguration; import org.owasp.encoder.Encode; import com.ibm.fhir.config.FHIRConfigHelper; @@ -61,6 +66,8 @@ import com.ibm.fhir.server.operation.FHIROperationRegistry; import com.ibm.fhir.server.registry.ServerRegistryResourceProvider; import com.ibm.fhir.server.util.FHIROperationUtil; +import com.ibm.fhir.term.graph.provider.GraphTermServiceProvider; +import com.ibm.fhir.term.service.FHIRTermService; @WebListener("IBM FHIR Server Servlet Context Listener") public class FHIRServletContextListener implements ServletContextListener { @@ -76,6 +83,8 @@ public class FHIRServletContextListener implements ServletContextListener { private static FHIRNotificationNATSPublisher natsPublisher = null; private static final String TXN_JNDI_NAME = "java:comp/UserTransaction"; + private GraphTermServiceProvider graphTermServiceProvider; + @Override public void contextInitialized(ServletContextEvent event) { if (log.isLoggable(Level.FINER)) { @@ -200,6 +209,20 @@ public void contextInitialized(ServletContextEvent event) { FHIRPersistenceInterceptorMgr.getInstance().addInterceptor(provider); } + Boolean graphTermServiceProviderEnabled = fhirConfig.getBooleanProperty(PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_ENABLED, Boolean.FALSE); + if (graphTermServiceProviderEnabled) { + log.info("Adding GraphTermServiceProvider..."); + PropertyGroup propertyGroup = fhirConfig.getPropertyGroup(PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_CONFIGURATION); + if (propertyGroup == null) { + log.log(Level.WARNING, "GraphTermServiceProvider configuration not found"); + } else { + Map map = new HashMap<>(); + propertyGroup.getProperties().stream().forEach(entry -> map.put(entry.getName(), entry.getValue())); + graphTermServiceProvider = new GraphTermServiceProvider(new MapConfiguration(map)); + FHIRTermService.getInstance().addProvider(graphTermServiceProvider); + } + } + // Finally, set our "initComplete" flag to true. event.getServletContext().setAttribute(FHIR_SERVER_INIT_COMPLETE, Boolean.TRUE); } catch(Throwable t) { @@ -412,6 +435,10 @@ public void contextDestroyed(ServletContextEvent event) { natsPublisher.shutdown(); natsPublisher = null; } + + if (graphTermServiceProvider != null) { + graphTermServiceProvider.getGraph().close(); + } } catch (Exception e) { } finally { if (log.isLoggable(Level.FINER)) { diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/registry/ServerRegistryResource.java b/fhir-server/src/main/java/com/ibm/fhir/server/registry/ServerRegistryResource.java index 363b80c2354..a12048a9217 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/registry/ServerRegistryResource.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/registry/ServerRegistryResource.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */ @@ -42,7 +42,7 @@ public static ServerRegistryResource from(Resource resource) { String id = resource.getId(); String url = FHIRRegistryUtil.getUrl(resource); String version = FHIRRegistryUtil.getVersion(resource); - if (url == null || version == null) { + if (url == null) { log.warning(String.format("Could not create ServerRegistryResource from Resource with resourceType: %s, id: %s, url: %s, and version: %s", resourceType.getSimpleName(), id, url, version)); return null; } @@ -56,6 +56,6 @@ public static ServerRegistryResource from(Resource resource) { SearchParameter searchParameter = (SearchParameter) resource; type = searchParameter.getType().getValue(); } - return new ServerRegistryResource(resourceType, id, url, Version.from(version), kind, type, resource); + return new ServerRegistryResource(resourceType, id, url, (version != null) ? Version.from(version) : FHIRRegistryResource.NO_VERSION, kind, type, resource); } } diff --git a/fhir-term-graph/pom.xml b/fhir-term-graph/pom.xml new file mode 100644 index 00000000000..9888f10be2c --- /dev/null +++ b/fhir-term-graph/pom.xml @@ -0,0 +1,62 @@ + + 4.0.0 + + com.ibm.fhir + fhir-parent + 4.6.0-SNAPSHOT + ../fhir-parent + + fhir-term-graph + + + ${project.groupId} + fhir-term + ${project.version} + + + org.janusgraph + janusgraph-core + 0.5.3 + + + org.janusgraph + janusgraph-berkeleyje + 0.5.3 + + + org.janusgraph + janusgraph-cql + 0.5.3 + + + org.janusgraph + janusgraph-lucene + 0.5.3 + + + org.janusgraph + janusgraph-es + 0.5.3 + + + org.apache.tinkerpop + gremlin-driver + 3.4.6 + + + org.slf4j + slf4j-nop + + + org.testng + testng + test + + + commons-cli + commons-cli + + + \ No newline at end of file diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java new file mode 100644 index 00000000000..ec590f431e9 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java @@ -0,0 +1,27 @@ +/* + * (C) Copyright IBM Corp. 2020 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph; + +import java.util.stream.Stream; + +import org.apache.commons.configuration.Configuration; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.janusgraph.core.JanusGraph; +import org.janusgraph.core.JanusGraphIndexQuery.Result; +import org.janusgraph.core.JanusGraphVertex; + +public interface FHIRTermGraph { + Configuration configuration(); + JanusGraph getJanusGraph(); + GraphTraversalSource traversal(); + default Stream> indexQuery(String query) { + return indexQuery(query, Integer.MAX_VALUE - 1); + } + Stream> indexQuery(String query, int limit); + void close(); + void drop(); +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraphFactory.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraphFactory.java new file mode 100644 index 00000000000..a2c37c79dbc --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraphFactory.java @@ -0,0 +1,28 @@ +/* + * (C) Copyright IBM Corp. 2020 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph; + +import org.apache.commons.configuration.Configuration; +import org.apache.commons.configuration.PropertiesConfiguration; + +import com.ibm.fhir.term.graph.impl.FHIRTermGraphImpl; + +public final class FHIRTermGraphFactory { + private FHIRTermGraphFactory() { } + + public static FHIRTermGraph open(String propFileName) { + try { + return open(new PropertiesConfiguration(propFileName)); + } catch (Exception e) { + throw new Error(e); + } + } + + public static FHIRTermGraph open(Configuration configuration) { + return new FHIRTermGraphImpl(configuration); + } +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java new file mode 100644 index 00000000000..637c2255998 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java @@ -0,0 +1,184 @@ +/* + * (C) Copyright IBM Corp. 2020, 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.impl; + +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.setRootLoggerLevel; +import static java.util.Objects.requireNonNull; + +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + +import org.apache.commons.configuration.Configuration; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.janusgraph.core.JanusGraph; +import org.janusgraph.core.JanusGraphFactory; +import org.janusgraph.core.JanusGraphIndexQuery.Result; +import org.janusgraph.core.JanusGraphVertex; +import org.janusgraph.core.PropertyKey; +import org.janusgraph.core.RelationType; +import org.janusgraph.core.schema.JanusGraphManagement; + +import com.ibm.fhir.term.graph.FHIRTermGraph; + +public class FHIRTermGraphImpl implements FHIRTermGraph { + private static final Logger log = Logger.getLogger(FHIRTermGraphImpl.class.getName()); + + private static final String STORAGE_READ_ONLY = "storage.read-only"; + + private JanusGraph graph; + private GraphTraversalSource traversal; + + public FHIRTermGraphImpl(Configuration configuration) { + requireNonNull(configuration); + try { + graph = open(configuration); + traversal = graph.traversal(); + } catch (Exception e) { + throw new Error(e); + } + } + + private JanusGraph open(Configuration configuration) { + log.info("Opening graph..."); + + boolean readOnly = configuration.getBoolean(STORAGE_READ_ONLY, false); + configuration.setProperty(STORAGE_READ_ONLY, false); + + setRootLoggerLevel(ch.qos.logback.classic.Level.INFO); + JanusGraph graph = JanusGraphFactory.open(configuration); + + if (!schemaExists(graph)) { + createSchema(graph); + } + + if (readOnly) { + graph.close(); + configuration.setProperty(STORAGE_READ_ONLY, true); + graph = JanusGraphFactory.open(configuration); + } + + return graph; + } + + private void createSchema(JanusGraph graph) { + log.info("Creating schema..."); + + JanusGraphManagement management = graph.openManagement(); + + // property keys (indexed) + PropertyKey version = management.makePropertyKey("version").dataType(String.class).make(); + PropertyKey code = management.makePropertyKey("code").dataType(String.class).make(); + PropertyKey codeLowerCase = management.makePropertyKey("codeLowerCase").dataType(String.class).make(); + PropertyKey display = management.makePropertyKey("display").dataType(String.class).make(); + PropertyKey displayLowerCase = management.makePropertyKey("displayLowerCase").dataType(String.class).make(); + PropertyKey value = management.makePropertyKey("value").dataType(String.class).make(); + PropertyKey url = management.makePropertyKey("url").dataType(String.class).make(); + + // property keys (not indexed) + management.makePropertyKey("count").dataType(Integer.class).make(); + management.makePropertyKey("group").dataType(Integer.class).make(); + management.makePropertyKey("language").dataType(String.class).make(); + management.makePropertyKey("system").dataType(String.class).make(); + management.makePropertyKey("use").dataType(String.class).make(); + management.makePropertyKey("caseSensitive").dataType(Boolean.class).make(); + management.makePropertyKey("designationUseSystem").dataType(String.class).make(); + + // vertex labels + management.makeVertexLabel("CodeSystem").make(); + management.makeVertexLabel("Concept").make(); + management.makeVertexLabel("Designation").make(); + management.makeVertexLabel("Property_").make(); + + // edge labels + management.makeEdgeLabel("concept").make(); + management.makeEdgeLabel("designation").make(); + management.makeEdgeLabel("property_").make(); + + // composite indexes + management.buildIndex("byCode", Vertex.class).addKey(code).buildCompositeIndex(); + management.buildIndex("byCodeLowerCase", Vertex.class).addKey(codeLowerCase).buildCompositeIndex(); + management.buildIndex("byDisplay", Vertex.class).addKey(display).buildCompositeIndex(); + management.buildIndex("byDisplayLowerCase", Vertex.class).addKey(displayLowerCase).buildCompositeIndex(); + management.buildIndex("byUrl", Vertex.class).addKey(url).buildCompositeIndex(); + management.buildIndex("byUrlAndVersion", Vertex.class).addKey(url).addKey(version).buildCompositeIndex(); + management.buildIndex("byValue", Vertex.class).addKey(value).buildCompositeIndex(); + + // mixed indexes + management.buildIndex("vertices", Vertex.class).addKey(display).addKey(value).buildMixedIndex("search"); + + log.info(System.lineSeparator() + management.printSchema()); + + management.commit(); + } + + private boolean schemaExists(JanusGraph graph) { + JanusGraphManagement management = graph.openManagement(); + if (management.getRelationTypes(RelationType.class).iterator().hasNext()) { + management.rollback(); + return true; + } + return false; + } + + @Override + public Configuration configuration() { + return graph.configuration(); + } + + @Override + public JanusGraph getJanusGraph() { + return graph; + } + + @Override + public GraphTraversalSource traversal() { + return traversal; + } + + @Override + public Stream> indexQuery(String query, int limit) { + return graph.indexQuery("vertices", query).limit(limit).vertexStream(); + } + + @Override + public void close() { + log.info("Closing graph..."); + try { + if (traversal != null) { + traversal.close(); + } + if (graph != null) { + graph.close(); + } + } catch (Exception e) { + log.log(Level.SEVERE, "An error occured while closing graph", e); + } finally { + traversal = null; + graph = null; + } + } + + @Override + public void drop() { + log.info("Dropping graph..."); + try { + if (traversal != null) { + traversal.close(); + } + if (graph != null) { + JanusGraphFactory.drop(graph); + } + } catch (Exception e) { + log.log(Level.SEVERE, "An error occurred while dropping graph"); + } finally { + traversal = null; + graph = null; + } + } +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/SnomedTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/SnomedTermGraphLoader.java new file mode 100644 index 00000000000..75c2bd1a568 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/SnomedTermGraphLoader.java @@ -0,0 +1,295 @@ +/* + * (C) Copyright IBM Corp. 2020, 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.loader; + +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toLabel; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.logging.Logger; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.MissingOptionException; +import org.apache.commons.cli.Options; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.janusgraph.core.JanusGraph; +import org.janusgraph.core.schema.JanusGraphManagement; + +import com.ibm.fhir.term.graph.FHIRTermGraph; +import com.ibm.fhir.term.graph.FHIRTermGraphFactory; + +public class SnomedTermGraphLoader { + private static final Logger log = Logger.getLogger(SnomedTermGraphLoader.class.getName()); + + private static final String PREFERRED = "900000000000548007"; + private static final String FULLY_SPECIFIED_NAME = "900000000000003001"; + + public static void main(String[] args) throws Exception { + Options options = null; + FHIRTermGraph graph = null; + try { + long start = System.currentTimeMillis(); + + options = new Options() + .addRequiredOption("file", null, true, "Configuration properties file") + .addRequiredOption("base", null, true, "SNOMED-CT base directory") + .addRequiredOption("concept", null, true, "SNOMED-CT concept file") + .addRequiredOption("relation", null, true, "SNOMED-CT relationship file") + .addRequiredOption("desc", null, true, "SNOMED-CT description file") + .addRequiredOption("lang", null, true, "SNOMED-CT language refset file"); + + CommandLineParser parser = new DefaultParser(); + CommandLine commandLine = parser.parse(options, args); + + String propFileName = commandLine.getOptionValue("file"); + + graph = FHIRTermGraphFactory.open(propFileName); + final JanusGraph janusGraph = graph.getJanusGraph(); + + GraphTraversalSource g = graph.traversal(); + + String baseDir = commandLine.getOptionValue("base"); + + AtomicInteger counter = new AtomicInteger(0); + Map vertexMap = new HashMap<>(250000); + + Vertex codeSystemVertex = g.addV("CodeSystem") + .property("url", "http://snomed.info/sct") + .property("caseSensitive", false) + .property("designationUseSystem", "http://snomed.info.sct") + .next(); + g.tx().commit(); + + // concept file + log.info("Processing concepts file..."); + String conceptFile = baseDir + "/" + commandLine.getOptionValue("concept"); + try (BufferedReader reader = new BufferedReader(new FileReader(conceptFile))) { + reader.lines().skip(1).forEach(new SnomedReleaseFileConsumer() { + @Override + public void processLine(String line) { + String[] tokens = line.split("\\t"); + String id = tokens[0]; + String active = tokens[2]; + + if ("1".equals(active)) { + if (!vertexMap.containsKey(id)) { + Vertex v = g.addV("Concept") + .property("code", id) + .property("codeLowerCase", normalize(id)) + .next(); + vertexMap.put(id, v); + g.V(codeSystemVertex).addE("concept").to(v).next(); + } + } + + if ((counter.get() % 10000) == 0) { + log.info("counter: " + counter.get()); + g.tx().commit(); + } + + counter.getAndIncrement(); + } + }); + + // commit any uncommitted work + g.tx().commit(); + } + + int count = counter.get(); + g.V(codeSystemVertex).property("count", count).next(); + g.tx().commit(); + + // language refset file + log.info("Processing language refset file..."); + Set preferred = new HashSet<>(500000); + String languageRefsetFile = baseDir + "/../Refset/Language/" + commandLine.getOptionValue("lang"); + try (BufferedReader reader = new BufferedReader(new FileReader(languageRefsetFile))) { + reader.lines().skip(1).forEach(new SnomedReleaseFileConsumer() { + @Override + public void processLine(String line) { + String[] tokens = line.split("\\t"); + String active = tokens[2]; + String referencedComponentId = tokens[5]; + String acceptabilityId = tokens[6]; + + if ("1".equals(active) && PREFERRED.equals(acceptabilityId)) { + preferred.add(referencedComponentId); + } + } + }); + } + + counter.set(0); + + // description file + log.info("Processing description file..."); + String descriptionFile = baseDir + "/" + commandLine.getOptionValue("desc"); + try (BufferedReader reader = new BufferedReader(new FileReader(descriptionFile))) { + reader.lines().skip(1).forEach(new SnomedReleaseFileConsumer() { + @Override + public void processLine(String line) { + String[] tokens = line.split("\\t"); + String id = tokens[0]; + String active = tokens[2]; + String conceptId = tokens[4]; + String typeId = tokens[6]; + String term = tokens[7]; + + Vertex v = vertexMap.get(conceptId); + + if ("1".equals(active) && v != null) { + if (preferred.contains(id) && !FULLY_SPECIFIED_NAME.equals(typeId)) { + // preferred term + g.V(v) + .property("display", term) + .property("displayLowerCase", normalize(term)) + .next(); + } + + Vertex w = g.addV("Designation") + .property("language", "en") + .property("use", typeId) + .property("value", term) + .next(); + + g.V(v).addE("designation").to(w).next(); + } + + if ((counter.get() % 10000) == 0) { + log.info("counter: " + counter.get()); + g.tx().commit(); + } + + counter.getAndIncrement(); + } + }); + + // commit any uncommitted work + g.tx().commit(); + } + + counter.set(0); + + // relationship file + log.info("Processing relationship file..."); + String relationshipFile = baseDir + "/" + commandLine.getOptionValue("relation"); + try (BufferedReader reader = new BufferedReader(new FileReader(relationshipFile))) { + reader.lines().skip(1).forEach(new SnomedReleaseFileConsumer() { + @Override + public void processLine(String line) { + String[] tokens = line.split("\\t"); + String active = tokens[2]; + String sourceId = tokens[4]; + String destinationId = tokens[5]; + String relationshipGroup = tokens[6]; + String typeId = tokens[7]; + + if ("1".equals(active)) { + Vertex u = vertexMap.get(sourceId); + Vertex v = vertexMap.get(destinationId); + Vertex w = vertexMap.get(typeId); + + if (u != null && v != null && w != null) { + String display = (String) g.V(w).values("display").next(); + String label = toLabel(display); + + if (janusGraph.getEdgeLabel(label) == null) { + log.info("Adding label: " + label); + JanusGraphManagement management = janusGraph.openManagement(); + management.makeEdgeLabel(label).make(); + management.commit(); + } + + Edge e = g.V(u).addE(label).to(v).next(); + + if (!"0".equals(relationshipGroup)) { + g.E(e).property("group", Integer.parseInt(relationshipGroup)).next(); + } + } + } + + if ((counter.get() % 10000) == 0) { + log.info("counter: " + counter.get()); + g.tx().commit(); + } + + counter.getAndIncrement(); + } + }); + + // commit any uncommitted work + g.tx().commit(); + } + + long end = System.currentTimeMillis(); + + log.info("Loading time (milliseconds): " + (end - start)); + + graph.close(); + graph = null; + } catch (MissingOptionException e) { + System.out.println("MissingOptionException: " + e.getMessage()); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("SnomedTermGraphLoader", options); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("An error occurred: " + e.getMessage()); + } finally { + if (graph != null) { + graph.close(); + } + } + } + + private static abstract class SnomedReleaseFileConsumer implements Consumer { + private final List lines = new ArrayList<>(); + private String previousId = null; + + @Override + public void accept(String line) { + if (collect(line)) { + lines.add(line); + } else { + processLines(Collections.unmodifiableList(lines)); + lines.clear(); + lines.add(line); + } + } + + private boolean collect(String line) { + String[] fields = line.split("\\t"); + String id = fields[0]; + if (!id.equals(previousId)) { + previousId = id; + return lines.isEmpty(); + } + return true; + } + + private void processLines(List lines) { + processLine(lines.get(lines.size() - 1)); + } + + // template method + public abstract void processLine(String line); + } +} 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 new file mode 100644 index 00000000000..4b9024b5188 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/provider/GraphTermServiceProvider.java @@ -0,0 +1,217 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.provider; + +import static com.ibm.fhir.model.type.String.string; +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; +import static com.ibm.fhir.term.util.CodeSystemSupport.isCaseSensitive; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.apache.commons.configuration.Configuration; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.structure.Vertex; + +import com.ibm.fhir.model.resource.CodeSystem; +import com.ibm.fhir.model.resource.CodeSystem.Concept; +import com.ibm.fhir.model.resource.CodeSystem.Concept.Designation; +import com.ibm.fhir.model.type.Code; +import com.ibm.fhir.model.type.Coding; +import com.ibm.fhir.model.type.Uri; +import com.ibm.fhir.term.graph.FHIRTermGraph; +import com.ibm.fhir.term.graph.FHIRTermGraphFactory; +import com.ibm.fhir.term.spi.FHIRTermServiceProvider; + +public class GraphTermServiceProvider implements FHIRTermServiceProvider { + private final FHIRTermGraph graph; + private final GraphTraversalSource g; + + public GraphTermServiceProvider(Configuration configuration) { + graph = FHIRTermGraphFactory.open(configuration); + g = graph.traversal(); + } + + public FHIRTermGraph getGraph() { + return graph; + } + + @Override + public boolean isSupported(CodeSystem codeSystem) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + return hasVersion(hasUrl(g.V(), codeSystem.getUrl()), codeSystem.getVersion()).hasNext(); + } + + @Override + public boolean hasConcept(CodeSystem codeSystem, Code code) { + return whereCodeSystem(hasCode(g.V(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem).hasNext(); + } + + @Override + public Concept getConcept(CodeSystem codeSystem, Code code) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + return getConcept(codeSystem, code, true); + } + + private Concept getConcept(CodeSystem codeSystem, Code code, boolean includeDesignations) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + return createConcept( + codeSystem, + code.getValue(), + whereCodeSystem(hasCode(g.V(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem) + .elementMap() + .tryNext(), + includeDesignations); + } + + @Override + public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + boolean caseSensitive = isCaseSensitive(codeSystem); + if (codeA.equals(codeB) || (!caseSensitive && normalize(codeA.getValue()).equals(normalize(codeB.getValue())))) { + return true; + } + return whereCodeSystem(hasCode(g.V(), codeA.getValue(), caseSensitive), codeSystem) + .repeat(__.in("isA") + .simplePath()) + .until(hasCode(codeB.getValue(), caseSensitive)) + .hasNext(); + } + + @Override + public Set getConcepts(CodeSystem codeSystem) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + List concepts = new ArrayList<>(getCount(codeSystem)); + hasVersion(hasUrl(g.V(), codeSystem.getUrl()), codeSystem.getVersion()) + .out("concept") + .elementMap() + .toStream() + .forEach(elementMap -> concepts.add(createConcept(elementMap))); + return Collections.emptySet(); + } + + @Override + public Set closure(CodeSystem codeSystem, Code code) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + Set concepts = new LinkedHashSet<>(); + concepts.add(getConcept(codeSystem, code, false)); + whereCodeSystem(hasCode(g.V(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem) + .repeat(__.in("isA") + .simplePath() + .dedup()) + .emit() + .elementMap() + .toStream() + .forEach(elementMap -> concepts.add(createConcept(elementMap))); + return concepts; + } + + private int getCount(CodeSystem codeSystem) { + Optional> optional = hasVersion(hasUrl(g.V(), codeSystem.getUrl()), codeSystem.getVersion()) + .elementMap("count") + .tryNext(); + if (optional.isPresent()) { + return (Integer) optional.get().get("count"); + } + return -1; + } + + // anonymous graph traversal + private GraphTraversal hasCode(String code, boolean caseSensitive) { + if (caseSensitive) { + return __.has("code", code); + } + return __.has("codeLowerCase", normalize(code)); + } + + private GraphTraversal hasCode(GraphTraversal g, String code, boolean caseSensitive) { + if (caseSensitive) { + return g.has("code", code); + } + return g.has("codeLowerCase", normalize(code)); + } + + private GraphTraversal hasUrl(GraphTraversal g, Uri url) { + return g.has("url", url.getValue()); + } + + private GraphTraversal hasVersion(GraphTraversal g, com.ibm.fhir.model.type.String version) { + if (version != null) { + return g.has("version", version.getValue()); + } + return g; + } + + private GraphTraversal whereCodeSystem(GraphTraversal g, CodeSystem codeSystem) { + return g.where(hasVersion(hasUrl(__.in("concept").hasLabel("CodeSystem"), codeSystem.getUrl()), codeSystem.getVersion())); + } + + private Concept createConcept(CodeSystem codeSystem, String code, Optional> optional, boolean includeDesignations) { + if (optional.isPresent()) { + return createConcept(optional.get(), includeDesignations ? getDesignations(codeSystem, code) : Collections.emptyList()); + } + return null; + } + + private Concept createConcept(Map elementMap) { + return createConcept(elementMap, Collections.emptyList()); + } + + private Concept createConcept(Map elementMap, List designations) { + return Concept.builder() + .code(Code.of((String) elementMap.get("code"))) + .display(string((String) elementMap.get("display"))) + .designation(designations) + .build(); + } + + private Designation createDesignation(Map elementMap, String designationUseSystem) { + Designation.Builder builder = Designation.builder(); + if (elementMap.containsKey("language")) { + builder.language(Code.of((String) elementMap.get("language"))); + } + if (elementMap.containsKey("use")) { + builder.use(Coding.builder() + .system((designationUseSystem != null) ? Uri.of(designationUseSystem) : null) + .code(Code.of((String) elementMap.get("use"))) + .build()); + } + builder.value(string((String) elementMap.get("value"))); + return builder.build(); + } + + private List getDesignations(CodeSystem codeSystem, String code) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + List designations = new ArrayList<>(); + String designationUseSystem = getDesignationUseSystem(codeSystem); + whereCodeSystem(hasCode(g.V(), code, isCaseSensitive(codeSystem)), codeSystem) + .out("designation") + .elementMap() + .toStream() + .forEach(elementMap -> designations.add(createDesignation(elementMap, designationUseSystem))); + return designations; + } + + private String getDesignationUseSystem(CodeSystem codeSystem) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + Optional> optional = hasVersion(hasUrl(g.V(), codeSystem.getUrl()), codeSystem.getVersion()) + .elementMap("designationUseSystem") + .tryNext(); + if (optional.isPresent()) { + return (String) optional.get().get("designationUseSystem"); + } + return null; + } +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java new file mode 100644 index 00000000000..b4a1373ed2e --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java @@ -0,0 +1,51 @@ +/* + * (C) Copyright IBM Corp. 2020, 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.util; + +import java.text.Normalizer; +import java.text.Normalizer.Form; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; + +public class FHIRTermGraphUtil { + private final static Set RESERVED_WORDS = new HashSet<>(Arrays.asList("key", "vertex", "edge", "element", "property", "label")); + + private FHIRTermGraphUtil() { } + + public static boolean isReservedWord(String s) { + return RESERVED_WORDS.contains(s.toLowerCase()); + } + + public static String normalize(String value) { + if (value != null) { + return Normalizer.normalize(value, Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "").toLowerCase(); + } + return null; + } + + public static void setRootLoggerLevel(Level level) { + Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + rootLogger.setLevel(level); + } + + public static String toLabel(String typeName) { + List tokens = Arrays.asList(typeName.split(" |_|-")); + String label = tokens.stream() + .map(token -> token.substring(0, 1).toUpperCase() + token.substring(1)) + .collect(Collectors.joining("")); + label = label.substring(0, 1).toLowerCase() + label.substring(1); + return isReservedWord(label) ? label + "_" : label; + } +} diff --git a/fhir-term-graph/src/main/resources/conf/local-graph.properties b/fhir-term-graph/src/main/resources/conf/local-graph.properties new file mode 100644 index 00000000000..37c6c843f4e --- /dev/null +++ b/fhir-term-graph/src/main/resources/conf/local-graph.properties @@ -0,0 +1,12 @@ +#storage.backend=berkeleyje +#storage.directory=/tmp/data/graph +#index.search.backend=lucene +#index.search.directory=/tmp/data/searchindex +cache.tx-cache-size=100000 +cache.tx-dirty-size=10000 +ids.block-size=500000 +index.search.backend=elasticsearch +index.search.hostname=127.0.0.1:9200 +storage.backend=cql +storage.batch-loading=true +storage.hostname=127.0.0.1 \ No newline at end of file diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java new file mode 100644 index 00000000000..f6798964b94 --- /dev/null +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java @@ -0,0 +1,52 @@ +/* + * (C) Copyright IBM Corp. 2020 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.test; + +import java.util.stream.Stream; + +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.testng.annotations.Test; + +import com.ibm.fhir.term.graph.FHIRTermGraph; +import com.ibm.fhir.term.graph.FHIRTermGraphFactory; + +public class FHIRTermGraphTest { + @Test + public void testTermGraph() throws Exception { + FHIRTermGraph graph = FHIRTermGraphFactory.open("conf/local-graph.properties"); + + GraphTraversalSource g = graph.traversal(); + + g.V().drop().iterate(); + g.E().drop().iterate(); + + Vertex v1 = g.addV("Concept").property("code", "a").next(); + System.out.println(v1.id()); + Vertex v2 = g.addV("Concept").property("code", "b").next(); + System.out.println(v2.id()); + Vertex v3 = g.addV("Concept").property("code", "c").next(); + System.out.println(v3.id()); + + g.V(v1).addE("isA").from(v2).next(); + g.V(v2).addE("isA").from(v3).next(); + + Stream.concat( + g.V(v1) + .elementMap() + .toStream(), + g.V(v1) + .repeat(__.in("isA").simplePath()) + .emit() + .elementMap() + .toStream()) + .forEach(System.out::println); + + graph.close(); + } +} diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java new file mode 100644 index 00000000000..8dd5180fedf --- /dev/null +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java @@ -0,0 +1,55 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.test; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.configuration.MapConfiguration; + +import com.ibm.fhir.model.resource.CodeSystem; +import com.ibm.fhir.model.resource.CodeSystem.Concept; +import com.ibm.fhir.model.type.Code; +import com.ibm.fhir.model.type.Uri; +import com.ibm.fhir.model.type.code.CodeSystemContentMode; +import com.ibm.fhir.model.type.code.PublicationStatus; +import com.ibm.fhir.term.graph.provider.GraphTermServiceProvider; + +public class GraphTermServiceProviderTest { + public static void main(String[] args) throws Exception { + CodeSystem codeSystem = CodeSystem.builder() + .url(Uri.of("http://snomed.info/sct")) + .status(PublicationStatus.ACTIVE) + .content(CodeSystemContentMode.NOT_PRESENT) + .build(); + + System.out.println(codeSystem); + + Map map = new HashMap<>(); + map.put("storage.backend", "cql"); + map.put("storage.hostname", "127.0.0.1"); + map.put("index.search.backend", "elasticsearch"); + map.put("index.search.hostname", "127.0.0.1:9200"); + map.put("query.batch", true); + map.put("query.batch-property-prefetch", true); + map.put("query.fast-property", true); + map.put("storage.read-only", true); + + GraphTermServiceProvider provider = new GraphTermServiceProvider(new MapConfiguration(map)); + + Set concepts = provider.closure(codeSystem, Code.of("195967001")); + concepts.stream().forEach(System.out::println); + + System.out.println(provider.subsumes(codeSystem, Code.of("195967001"), Code.of("31387002"))); + System.out.println(provider.subsumes(codeSystem, Code.of("195967001"), Code.of("195967001"))); + + System.out.println(provider.getConcept(codeSystem, Code.of("195967001"))); + + provider.getGraph().close(); + } +} diff --git a/fhir-term-graph/src/test/resources/conf/local-graph.properties b/fhir-term-graph/src/test/resources/conf/local-graph.properties new file mode 100644 index 00000000000..6e1e4e75f8e --- /dev/null +++ b/fhir-term-graph/src/test/resources/conf/local-graph.properties @@ -0,0 +1,5 @@ +storage.backend=berkeleyje +storage.directory=target/data/graph +#storage.batch-loading=true +index.search.backend=lucene +index.search.directory=target/data/searchindex \ No newline at end of file diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java b/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java index 5a0933a3811..ee533956bd1 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java @@ -58,12 +58,12 @@ public boolean isSupported(CodeSystem codeSystem) { } @Override - public Concept findConcept(CodeSystem codeSystem, Code code) { - return null; + public boolean hasConcept(CodeSystem codeSystem, Code code) { + return false; } @Override - public Concept findConcept(CodeSystem codeSystem, Concept concept, Code code) { + public Concept getConcept(CodeSystem codeSystem, Code code) { return null; } @@ -73,7 +73,12 @@ public Set getConcepts(CodeSystem codeSystem) { } @Override - public Set getConcepts(CodeSystem codeSystem, Concept concept) { + public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { + return false; + } + + @Override + public Set closure(CodeSystem codeSystem, Code code) { return Collections.emptySet(); } }; @@ -84,12 +89,12 @@ private FHIRTermService() { } /** - * Register the given {@link FHIRTermServiceProvider} + * Add the given {@link FHIRTermServiceProvider} to the service * * @param provider * the term service provider */ - public void register(FHIRTermServiceProvider provider) { + public void addProvider(FHIRTermServiceProvider provider) { Objects.requireNonNull(provider); providers.add(provider); } @@ -118,33 +123,17 @@ public boolean isSupported(CodeSystem codeSystem) { } /** - * Find the concept in the provided code system that matches the specified code. + * Get the concept in the provided code system with the specified code. * * @param codeSystem * the code system to search * @param code * the code to match * @return - * the code system concept that matches the specified code, or null if no such concept exists + * the code system concept with the specified code, or null if no such concept exists */ - public Concept findConcept(CodeSystem codeSystem, Code code) { - return findProvider(codeSystem).findConcept(codeSystem, code); - } - - /** - * Find the concept in tree rooted by the provided concept that matches the specified code. - * - * @param codeSystem - * the code system - * @param concept - * the root of the hierarchy to search - * @param code - * the code to match - * @return - * the code system concept that matches the specified code, or null if not such concept exists - */ - public Concept findConcept(CodeSystem codeSystem, Concept concept, Code code) { - return findProvider(codeSystem).findConcept(codeSystem, concept, code); + public Concept getConcept(CodeSystem codeSystem, Code code) { + return findProvider(codeSystem).getConcept(codeSystem, code); } /** @@ -160,21 +149,6 @@ public Set getConcepts(CodeSystem codeSystem) { return findProvider(codeSystem).getConcepts(codeSystem); } - /** - * Get a set containing {@link CodeSystem.Concept} instances where all structural - * hierarchies have been flattened. - * - * @param codeSystem - * the code system - * @param concept - * the root of the hierarchy containing the Concept instances to be flattened - * @return - * flattened set of Concept instances for the given tree - */ - public Set getConcepts(CodeSystem codeSystem, Concept concept) { - return findProvider(codeSystem).getConcepts(codeSystem, concept); - } - /** * Indicates whether the given value set is expandable * @@ -279,7 +253,7 @@ public LookupOutcome lookup(Coding coding, LookupParameters parameters) { java.lang.String url = (version != null) ? system.getValue() + "|" + version : system.getValue(); CodeSystem codeSystem = CodeSystemSupport.getCodeSystem(url); if (codeSystem != null) { - Concept concept = findProvider(codeSystem).findConcept(codeSystem, code); + Concept concept = findProvider(codeSystem).getConcept(codeSystem, code); if (concept != null) { return LookupOutcome.builder() .name((codeSystem.getName() != null) ? codeSystem.getName() : STRING_DATA_ABSENT_REASON_UNKNOWN) @@ -317,6 +291,22 @@ public LookupOutcome lookup(Coding coding) { return lookup(coding, LookupParameters.EMPTY); } + /** + * Find the concept in tree rooted by the provided concept that matches the specified code. + * + * @param codeSystem + * the code system + * @param codeA + * the code "A" + * @param codeB + * the code "B" + * @return + * true if the concept represented by code "A" subsumes the concept represented by code "B", false otherwise + */ + public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { + return findProvider(codeSystem).subsumes(codeSystem, codeA, codeB); + } + /** * Perform a subsumption test to determine if the code system concept represented by the given coding "A" subsumes * the code system concept represented by the given coding "B" @@ -350,16 +340,13 @@ public ConceptSubsumptionOutcome subsumes(Coding codingA, Coding codingB) { CodeSystem codeSystem = CodeSystemSupport.getCodeSystem(url); if (codeSystem != null && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { FHIRTermServiceProvider provider = findProvider(codeSystem); - Concept conceptA = provider.findConcept(codeSystem, codeA); - if (conceptA != null) { - Concept conceptB = provider.findConcept(codeSystem, conceptA, codeB); - if (conceptB != null) { - return conceptA.equals(conceptB) ? ConceptSubsumptionOutcome.EQUIVALENT : ConceptSubsumptionOutcome.SUBSUMES; + if (provider.hasConcept(codeSystem, codeA)) { + boolean subsumes = provider.subsumes(codeSystem, codeA, codeB); + if (subsumes) { + return codeA.equals(codeB) ? ConceptSubsumptionOutcome.EQUIVALENT : ConceptSubsumptionOutcome.SUBSUMES; } - conceptB = provider.findConcept(codeSystem, codeB); - if (conceptB != null) { - conceptA = provider.findConcept(codeSystem, conceptB, codeA); - return (conceptA != null) ? ConceptSubsumptionOutcome.SUBSUMED_BY : ConceptSubsumptionOutcome.NOT_SUBSUMED; + if (provider.hasConcept(codeSystem, codeB)) { + return provider.subsumes(codeSystem, codeB, codeA) ? ConceptSubsumptionOutcome.SUBSUMED_BY : ConceptSubsumptionOutcome.NOT_SUBSUMED; } } } @@ -368,8 +355,8 @@ public ConceptSubsumptionOutcome subsumes(Coding codingA, Coding codingB) { return null; } - public Set closure(CodeSystem codeSystem, Concept concept) { - return findProvider(codeSystem).getConcepts(codeSystem, concept); + public Set closure(CodeSystem codeSystem, Code code) { + return findProvider(codeSystem).closure(codeSystem, code); } /** @@ -390,9 +377,8 @@ public Set closure(Coding coding) { CodeSystem codeSystem = CodeSystemSupport.getCodeSystem(url); if (codeSystem != null && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { FHIRTermServiceProvider provider = findProvider(codeSystem); - Concept concept = provider.findConcept(codeSystem, code); - if (concept != null) { - return provider.getConcepts(codeSystem, concept); + if (provider.hasConcept(codeSystem, code)) { + return provider.closure(codeSystem, code); } } } diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java b/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java index b0f8a210066..52943fa20bd 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java @@ -25,22 +25,29 @@ public boolean isSupported(CodeSystem codeSystem) { } @Override - public Concept findConcept(CodeSystem codeSystem, Code code) { + public boolean hasConcept(CodeSystem codeSystem, Code code) { + return getConcept(codeSystem, code) != null; + } + + @Override + public Concept getConcept(CodeSystem codeSystem, Code code) { return CodeSystemSupport.findConcept(codeSystem, code); } @Override - public Concept findConcept(CodeSystem codeSystem, Concept concept, Code code) { - return CodeSystemSupport.findConcept(codeSystem, concept, code); + public Set getConcepts(CodeSystem codeSystem) { + return CodeSystemSupport.getConcepts(codeSystem); } @Override - public Set getConcepts(CodeSystem codeSystem, Concept concept) { - return CodeSystemSupport.getConcepts(concept); + public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { + Concept concept = CodeSystemSupport.findConcept(codeSystem, codeA); + return (CodeSystemSupport.findConcept(codeSystem, concept, codeB) != null); } @Override - public Set getConcepts(CodeSystem codeSystem) { - return CodeSystemSupport.getConcepts(codeSystem); + public Set closure(CodeSystem codeSystem, Code code) { + Concept concept = CodeSystemSupport.findConcept(codeSystem, code); + return CodeSystemSupport.getConcepts(concept); } } diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/spi/FHIRTermServiceProvider.java b/fhir-term/src/main/java/com/ibm/fhir/term/spi/FHIRTermServiceProvider.java index 06b67d75a6a..f82bbb2ee2e 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/spi/FHIRTermServiceProvider.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/spi/FHIRTermServiceProvider.java @@ -23,42 +23,45 @@ public interface FHIRTermServiceProvider { */ boolean isSupported(CodeSystem codeSystem); + + boolean hasConcept(CodeSystem codeSystem, Code code); + /** - * Find the concept in the provided code system that matches the specified code. + * Get the concept in the provided code system with the specified code. * * @param codeSystem - * the code system to search + * the code system * @param code - * the code to match + * the code * @return - * the code system concept that matches the specified code, or null if no such concept exists + * the code system concept with the specified code, or null if no such concept exists */ - Concept findConcept(CodeSystem codeSystem, Code code); + Concept getConcept(CodeSystem codeSystem, Code code); /** - * Find the concept in tree rooted by the provided concept that matches the specified code. + * Get a set containing {@link CodeSystem.Concept} instances where all structural + * hierarchies have been flattened. * * @param codeSystem * the code system - * @param concept - * the root of the hierarchy to search - * @param code - * the code to match * @return - * the code system concept that matches the specified code, or null if not such concept exists + * flattened list of Concept instances for the given code system */ - Concept findConcept(CodeSystem codeSystem, Concept concept, Code code); + Set getConcepts(CodeSystem codeSystem); /** - * Get a set containing {@link CodeSystem.Concept} instances where all structural - * hierarchies have been flattened. + * Find the concept in tree rooted by the provided concept that matches the specified code. * * @param codeSystem * the code system + * @param codeA + * the root of the hierarchy to search + * @param codeB + * the code to match * @return - * flattened list of Concept instances for the given code system + * the code system concept that matches the specified code, or null if not such concept exists */ - Set getConcepts(CodeSystem codeSystem); + boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB); /** * Get a set containing {@link CodeSystem.Concept} instances where all structural @@ -66,10 +69,10 @@ public interface FHIRTermServiceProvider { * * @param codeSystem * the code system - * @param concept + * @param code * the root of the hierarchy containing the Concept instances to be flattened * @return * flattened set of Concept instances for the given tree */ - Set getConcepts(CodeSystem codeSystem, Concept concept); + Set closure(CodeSystem codeSystem, Code code); } \ No newline at end of file diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/spi/LookupOutcome.java b/fhir-term/src/main/java/com/ibm/fhir/term/spi/LookupOutcome.java index 51ebb895148..2ebd3591a56 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/spi/LookupOutcome.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/spi/LookupOutcome.java @@ -105,13 +105,13 @@ public Parameters toParameters() { .name(string("version")) .value(version) .build()); - - parametersBuilder.parameter(Parameter.builder() - .name(string("display")) - .value(display) - .build()); } + parametersBuilder.parameter(Parameter.builder() + .name(string("display")) + .value(display) + .build()); + for (Designation designation : this.designation) { Parameter.Builder designationParameterBuilder = Parameter.builder(); diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java b/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java index 7390cdc58f3..e7440da8cc4 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java @@ -280,7 +280,7 @@ private static boolean convertsToBoolean(String value) { private static ConceptFilter createDescendentOfFilter(CodeSystem codeSystem, Filter filter) { if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - Concept concept = FHIRTermService.getInstance().findConcept(codeSystem, code(filter.getValue())); + Concept concept = FHIRTermService.getInstance().getConcept(codeSystem, code(filter.getValue())); if (concept != null) { return new DescendentOfFilter(codeSystem, concept); } @@ -309,7 +309,7 @@ private static ConceptFilter createExistsFilter(CodeSystem codeSystem, Filter fi private static ConceptFilter createGeneralizesFilter(CodeSystem codeSystem, Filter filter) { if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - Concept concept = FHIRTermService.getInstance().findConcept(codeSystem, code(filter.getValue())); + Concept concept = FHIRTermService.getInstance().getConcept(codeSystem, code(filter.getValue())); if (concept != null) { return new GeneralizesFilter(codeSystem, concept); } @@ -329,7 +329,7 @@ private static ConceptFilter createInFilter(CodeSystem codeSystem, Filter filter private static ConceptFilter createIsAFilter(CodeSystem codeSystem, Filter filter) { if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - Concept concept = FHIRTermService.getInstance().findConcept(codeSystem, code(filter.getValue())); + Concept concept = FHIRTermService.getInstance().getConcept(codeSystem, code(filter.getValue())); if (concept != null) { return new IsAFilter(codeSystem, concept); } @@ -339,7 +339,7 @@ private static ConceptFilter createIsAFilter(CodeSystem codeSystem, Filter filte private static ConceptFilter createIsNotAFilter(CodeSystem codeSystem, Filter filter) { if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - Concept concept = FHIRTermService.getInstance().findConcept(codeSystem, code(filter.getValue())); + Concept concept = FHIRTermService.getInstance().getConcept(codeSystem, code(filter.getValue())); if (concept != null) { return new IsNotAFilter(codeSystem, concept); } @@ -494,12 +494,12 @@ public EqualsFilter(CodeSystem codeSystem, Code property, String value) { this.value = value; children = new LinkedHashSet<>(); if ("parent".equals(property.getValue())) { - Concept parent = FHIRTermService.getInstance().findConcept(codeSystem, code(value)); + Concept parent = FHIRTermService.getInstance().getConcept(codeSystem, code(value)); if (parent != null) { children.addAll(parent.getConcept()); } } - this.child = "child".equals(property.getValue()) ? FHIRTermService.getInstance().findConcept(codeSystem, code(value)) : null; + this.child = "child".equals(property.getValue()) ? FHIRTermService.getInstance().getConcept(codeSystem, code(value)) : null; } @Override @@ -548,7 +548,7 @@ public GeneralizesFilter(CodeSystem codeSystem, Concept concept) { @Override public boolean accept(Concept concept) { - return (FHIRTermService.getInstance().findConcept(codeSystem, concept, this.concept.getCode()) != null); + return FHIRTermService.getInstance().subsumes(codeSystem, concept.getCode(), this.concept.getCode()); } } @@ -580,7 +580,7 @@ public IsAFilter(CodeSystem codeSystem, Concept concept) { @Override public boolean accept(Concept concept) { - return (FHIRTermService.getInstance().findConcept(codeSystem, this.concept, concept.getCode()) != null); + return FHIRTermService.getInstance().subsumes(codeSystem, this.concept.getCode(), concept.getCode()); } } From 2edd11b79d1bc279eba0f17c4bcc005ededfbd5b Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Fri, 26 Feb 2021 14:01:39 -0500 Subject: [PATCH 02/50] Issue #1980 - updated fhir-parent/pom.xml Signed-off-by: John T.E. Timm --- fhir-parent/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/fhir-parent/pom.xml b/fhir-parent/pom.xml index 5241c035a81..ae802e5ce21 100644 --- a/fhir-parent/pom.xml +++ b/fhir-parent/pom.xml @@ -47,6 +47,7 @@ ../fhir-model ../fhir-registry ../fhir-term + ../fhir-term-graph ../fhir-path ../fhir-profile ../fhir-validation From 17b7ebd603aca8d4df754ff64f4c48bf861202b6 Mon Sep 17 00:00:00 2001 From: "Adam T. Clark" Date: Mon, 1 Mar 2021 20:02:53 -0600 Subject: [PATCH 03/50] Implement UMLSTermGraphLoader Signed-off-by: Adam T. Clark --- .../graph/loader/UMLSTermGraphLoader.java | 384 ++++++++++++++++++ .../conf/umlsCodesystemMap.properties | 1 + .../conf/umlsSourceCaseSensitivity.txt | 3 + 3 files changed, 388 insertions(+) create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/UMLSTermGraphLoader.java create mode 100644 fhir-term-graph/src/main/resources/conf/umlsCodesystemMap.properties create mode 100644 fhir-term-graph/src/main/resources/conf/umlsSourceCaseSensitivity.txt diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/UMLSTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/UMLSTermGraphLoader.java new file mode 100644 index 00000000000..1daf9c1d7f6 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/UMLSTermGraphLoader.java @@ -0,0 +1,384 @@ +/* + * (C) Copyright IBM Corp. 2020, 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.loader; + +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toLabel; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.MissingOptionException; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.janusgraph.core.JanusGraph; +import org.janusgraph.core.schema.JanusGraphManagement; + +import com.ibm.fhir.term.graph.FHIRTermGraph; +import com.ibm.fhir.term.graph.FHIRTermGraphFactory; + +/* + * This class will load UMLS concepts and relationships into a JanusGraph. + */ +public class UMLSTermGraphLoader { + private static final Logger LOG = Logger.getLogger(UMLSTermGraphLoader.class.getName()); + + private static final String UMLS_DELIMITER = "\\|"; + + /** + * Load UMLS data using properties provided in arguments + * + * @param args + */ + public static void main(String[] args) { + UMLSTermGraphLoader loader = null; + Options options = null; + try { + long start = System.currentTimeMillis(); + + // Parse arguments + options = new Options() + .addRequiredOption("file", null, true, "Configuration properties file") + .addRequiredOption("base", null, true, "UMLS base directory") + .addRequiredOption("conceptFile", null, true, "UMLS concept (MRCONSO) file") + .addRequiredOption("relationFile", null, true, "UMLS relationship (MRREL) file") + .addRequiredOption("sourceAttributeFile", null, true, "UMLS source attribute (MRSAB) file"); + + CommandLineParser parser = new DefaultParser(); + CommandLine commandLine = parser.parse(options, args); + + String baseDir = commandLine.getOptionValue("base"); + String relationshipFile = baseDir + "/" + commandLine.getOptionValue("relationFile"); + String conceptFile = baseDir + "/" + commandLine.getOptionValue("conceptFile"); + String sabFile = baseDir + "/" + commandLine.getOptionValue("sourceAttributeFile"); + String propFileName = commandLine.getOptionValue("file"); + + loader = new UMLSTermGraphLoader(propFileName, conceptFile, relationshipFile, sabFile); + loader.load(); + + long end = System.currentTimeMillis(); + LOG.info("Loading time (milliseconds): " + (end - start)); + } catch (MissingOptionException e) { + LOG.log(Level.SEVERE, "MissingOptionException: ", e); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("UMLSTermGraphLoader", options); + } catch (Exception e) { + LOG.log(Level.SEVERE, "An error occurred: " + e.getMessage()); + } finally { + if (loader != null) { + loader.close(); + } + } + } + + // Map to track AUI to SCUI relationships, since MRREL uses AUI, but granularity of concepts used in MRCONSO is at SCUI level + private Map auiToScuiMap = new ConcurrentHashMap<>(1000000); + + // Map of code system name to preferred label, configured in properties file + private Properties codeSystemMap = new Properties(); + + // Set of code systems which are case sensitive, configured in properties file + private Set caseSensitiveCodeSystems = new HashSet<>(); + + // Map of code system id to corresponding vertex + private Map codeSystemVertices = new ConcurrentHashMap<>(); + + // Name of file containing UMLS concept data + private String conceptFile = null; + + private GraphTraversalSource g = null; + private FHIRTermGraph graph = null; + private JanusGraph janusGraph = null; + + // Name of file containing UMLS concept relationship data + private String relationshipFile = null; + + // Map of abbreviated source name to the current version of that source + private Map sabToVersion = new HashMap<>(); + + // Name of file containing source data + private String sourceAttributeFile = null; + + // Map of concept name to corresponding vertex + private Map vertexMap = null; + + /** + * Initialize a UMLSTermGraphLoader + * + * @param propFileName + * @param conceptFile + * @param relationshipFile + * @param sourceAttributeFile + * @throws ParseException + * @throws IOException + * @throws FileNotFoundException + */ + public UMLSTermGraphLoader(String propFileName, String conceptFile, String relationshipFile, String sourceAttributeFile) { + this.conceptFile = conceptFile; + this.relationshipFile = relationshipFile; + this.sourceAttributeFile = sourceAttributeFile; + + graph = FHIRTermGraphFactory.open(propFileName); + janusGraph = graph.getJanusGraph(); + g = graph.traversal(); + vertexMap = new HashMap<>(250000); + } + + /** + * Loads UMLS data into JanusGraph + * + * @throws ParseException + * @throws IOException + * @throws FileNotFoundException + */ + public void load() throws ParseException, IOException, FileNotFoundException { + loadSourceAttributes(); + loadCaseSensitiveCodeSystems(); + loadConcepts(); + loadRelations(); + } + + /** + * Close loader, thereby closing connection to JanusGraph + */ + public void close() { + if (graph != null) { + graph.close(); + graph = null; + } + } + + /** + * Create a code system vertex for the provided abbreviated source name + * + * @param sab + * @return + */ + private final Vertex createCodeSystemVertex(String sab) { + String version = sabToVersion.get(sab); + String url = (String) codeSystemMap.getOrDefault(sab, sab); + + boolean caseSensitive = false; + if (caseSensitiveCodeSystems.contains(sab)) { + caseSensitive = true; + } + Vertex csv = g.addV("CodeSystem").property("url", url).property("version", version).property("caseSensitive", caseSensitive).next(); + g.tx().commit(); + return csv; + + } + + public JanusGraph getJanusGraph() { + return janusGraph; + } + + /** + * Loads all UMLS concept data from the provided conceptFile + * + * @throws FileNotFoundException + * @throws IOException + */ + private void loadConcepts() throws FileNotFoundException, IOException { + // MRCONSO.RRF + // CUI, LAT, TS, LUI, STT, SUI, ISPREF, AUI, SAUI, SCUI, SDUI, SAB, TTY, CODE, STR, SRL, SUPPRESS, CVF + // https://www.ncbi.nlm.nih.gov/books/NBK9685/table/ch03.T.concept_names_and_sources_file_mr/ + // + LOG.info("Loading concepts....."); + + Map sabCounterMap = new HashMap<>(); + + try (BufferedReader reader = new BufferedReader(new FileReader(conceptFile))) { + reader.lines().forEach(line -> { + String[] tokens = line.split(UMLS_DELIMITER); + String lat = tokens[1]; + String aui = tokens[7]; + String scui = tokens[9]; + String sab = tokens[11]; + String tty = tokens[12]; + String str = tokens[14]; + String suppress = tokens[16]; + if (!"O".equals(suppress)) { + + auiToScuiMap.put(aui, scui); + + Vertex codeSystemVertex = codeSystemVertices.computeIfAbsent(sab, s -> createCodeSystemVertex(s)); + + AtomicInteger counter = sabCounterMap.computeIfAbsent(sab, s -> new AtomicInteger(0)); + counter.incrementAndGet(); + + Vertex v = null; + if (vertexMap.containsKey(scui)) { + v = vertexMap.get(scui); + } else { + String codeLowerCase = normalize(scui); + v = g.addV("Concept").property("code", scui).property("codeLowerCase", codeLowerCase).next(); + vertexMap.put(scui, v); + + g.V(codeSystemVertex).addE("concept").to(v).next(); + } + if (v == null) { + LOG.severe("Could not find SCUI in vertexMap"); + } else { + if (tty.equals("PT")) { // Preferred entries provide preferred name and language + String displayLowerCase = normalize(str); + v.property("display", str); + v.property("displayLowerCase", displayLowerCase); + v.property("language", lat); + + } + // add new designation + Vertex w = g.addV("Designation").property("language", lat).property("value", str).next(); + g.V(v).addE("designation").to(w).next(); + + if ((sabCounterMap.values().stream().collect(Collectors.summingInt(AtomicInteger::get)) % 10000) == 0) { + String counters = sabCounterMap.entrySet().stream().map(e -> e.getKey() + ":" + e.getValue()).collect(Collectors.joining(",")); + LOG.info("counter: " + counters); + g.tx().commit(); + } + + } + } + }); + + for (Entry entry : sabCounterMap.entrySet()) { + Vertex codeSystemVertex = codeSystemVertices.get(entry.getKey()); + g.V(codeSystemVertex).property("count", entry.getValue().get()).next(); + } + // commit any uncommitted work + g.tx().commit(); + } + + g.tx().commit(); + LOG.info("Done loading concepts....."); + } + + /** + * Loads all UMLS relationship data from the provided relationshipFile + * + * @throws FileNotFoundException + * @throws IOException + */ + private void loadRelations() throws FileNotFoundException, IOException { + // MRREL + // CUI1, AUI1, STYPE1, REL, CUI2, AUI2, STYPE2, RELA, RUI, SRUI, SAB, SL, RG,DIR, SUPPRESS, CVF + // https://www.ncbi.nlm.nih.gov/books/NBK9685/table/ch03.T.related_concepts_file_mrrel_rrf/ + // + LOG.info("Loading relations....."); + + AtomicInteger counter = new AtomicInteger(0); + + try (BufferedReader reader = new BufferedReader(new FileReader(relationshipFile))) { + reader.lines().forEach(line -> { + String[] tokens = line.split(UMLS_DELIMITER); + String aui1 = tokens[1]; + String rela = tokens[7]; + String aui2 = tokens[5]; + String dir = tokens[13]; + String suppress = tokens[14]; + if (!"N".equals(dir) && !"O".equals(suppress)) { // Don't load relations that are not in source order or suppressed + + String scui1 = auiToScuiMap.get(aui1); + String scui2 = auiToScuiMap.get(aui2); + if (scui1 != null && scui2 != null) { + Vertex v1 = vertexMap.get(scui1); + Vertex v2 = vertexMap.get(scui2); + + if (v1 != null && v2 != null) { + String label = toLabel(rela); + + if (janusGraph.getEdgeLabel(label) == null) { + LOG.info("Adding label: " + label); + JanusGraphManagement management = janusGraph.openManagement(); + management.makeEdgeLabel(label).make(); + management.commit(); + } + + Edge e = g.V(v2).addE(label).to(v1).next(); + g.E(e).next(); + } + + if ((counter.get() % 10000) == 0) { + LOG.info("counter: " + counter.get()); + g.tx().commit(); + } + + counter.getAndIncrement(); + } + } + }); + + // commit any uncommitted work + g.tx().commit(); + LOG.info("Done loading relations....."); + } + } + + /** + * Loads all UMLS source attribute data from the provided sourceAttributeFile + * + * @throws IOException + */ + private void loadSourceAttributes() throws IOException { + try (Reader codeSystemReader = new InputStreamReader(getClass().getClassLoader().getResourceAsStream("conf/umlsCodesystemMap.properties"))) { + // Load UMLS name to preferred CodeSystem name map + codeSystemMap.load(codeSystemReader); + } + + try (BufferedReader reader = new BufferedReader(new FileReader(sourceAttributeFile))) { + // Load latest version for code systems in UMLS + reader.lines().forEach(line -> { + String[] tokens = line.split(UMLS_DELIMITER); + String rsab = tokens[3]; + String sver = tokens[6]; + String curver = tokens[21]; + if ("Y".equals(curver)) { + if ("SNOMEDCT_US".equals(rsab)) { + // special case version for SNOMED + sver = "http://snomed.info/sct/731000124108/version/" + sver.replaceAll("_", ""); + } + sabToVersion.put(rsab, sver); + } + }); + } + } + + /** + * Loads configuration of code systems noted to be case sensitive + * + * @throws IOException + */ + private void loadCaseSensitiveCodeSystems() throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getClassLoader().getResourceAsStream("conf/umlsSourceCaseSensitivity.txt")))) { + String line = reader.readLine().trim(); + if (!line.isEmpty()) { + caseSensitiveCodeSystems.add(reader.readLine()); + } + } + } +} \ No newline at end of file diff --git a/fhir-term-graph/src/main/resources/conf/umlsCodesystemMap.properties b/fhir-term-graph/src/main/resources/conf/umlsCodesystemMap.properties new file mode 100644 index 00000000000..3b1e5b21e8c --- /dev/null +++ b/fhir-term-graph/src/main/resources/conf/umlsCodesystemMap.properties @@ -0,0 +1 @@ +SNOMEDCT_US=http://snomed.info/sct \ No newline at end of file diff --git a/fhir-term-graph/src/main/resources/conf/umlsSourceCaseSensitivity.txt b/fhir-term-graph/src/main/resources/conf/umlsSourceCaseSensitivity.txt new file mode 100644 index 00000000000..d1ea8db470c --- /dev/null +++ b/fhir-term-graph/src/main/resources/conf/umlsSourceCaseSensitivity.txt @@ -0,0 +1,3 @@ +HL7V2.5 +HL7V3.0 +NCI_UCUM \ No newline at end of file From 366b0cdc949e8e6f8506cfeb499c15025328f9df Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 2 Mar 2021 15:04:49 -0500 Subject: [PATCH 04/50] Issue #1980 - updates Signed-off-by: John T.E. Timm --- .../term/graph/impl/FHIRTermGraphImpl.java | 37 +- .../loader/CodeSystemTermGraphLoader.java | 121 +++++ .../provider/GraphTermServiceProvider.java | 6 + .../graph/serialize/BooleanSerializer.java | 25 + .../term/graph/serialize/CodeSerializer.java | 32 ++ .../graph/serialize/DateTimeSerializer.java | 31 ++ .../graph/serialize/DecimalSerializer.java | 25 + .../graph/serialize/IntegerSerializer.java | 25 + .../graph/serialize/StringSerializer.java | 33 ++ .../test/CodeSystemTermGraphLoaderTest.java | 35 ++ .../term/graph/test/FHIRTermGraphTest.java | 20 +- .../test/resources/JSON/CodeSystem-cs5.json | 79 ++++ .../fhir/term/service/FHIRTermService.java | 37 +- .../provider/DefaultTermServiceProvider.java | 7 + .../term/spi/FHIRTermServiceProvider.java | 28 +- .../ibm/fhir/term/util/CodeSystemSupport.java | 435 ++++++++++++++++-- .../ibm/fhir/term/util/ValueSetSupport.java | 353 +------------- 17 files changed, 940 insertions(+), 389 deletions(-) create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/CodeSystemTermGraphLoader.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/BooleanSerializer.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/CodeSerializer.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DateTimeSerializer.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DecimalSerializer.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/IntegerSerializer.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/StringSerializer.java create mode 100644 fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java create mode 100644 fhir-term-graph/src/test/resources/JSON/CodeSystem-cs5.json diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java index 637c2255998..9270dfbd08b 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java @@ -24,6 +24,9 @@ import org.janusgraph.core.RelationType; import org.janusgraph.core.schema.JanusGraphManagement; +import com.ibm.fhir.model.type.Code; +import com.ibm.fhir.model.type.DateTime; +import com.ibm.fhir.model.type.Decimal; import com.ibm.fhir.term.graph.FHIRTermGraph; public class FHIRTermGraphImpl implements FHIRTermGraph { @@ -50,6 +53,8 @@ private JanusGraph open(Configuration configuration) { boolean readOnly = configuration.getBoolean(STORAGE_READ_ONLY, false); configuration.setProperty(STORAGE_READ_ONLY, false); + addCustomAttributeSerializers(configuration); + setRootLoggerLevel(ch.qos.logback.classic.Level.INFO); JanusGraph graph = JanusGraphFactory.open(configuration); @@ -66,6 +71,21 @@ private JanusGraph open(Configuration configuration) { return graph; } + private void addCustomAttributeSerializers(Configuration configuration) { + configuration.setProperty("attributes.custom.attribute1.attribute-class", "com.ibm.fhir.model.type.Boolean"); + configuration.setProperty("attributes.custom.attribute1.serializer-class", "com.ibm.fhir.term.graph.serialize.BooleanSerializer"); + configuration.setProperty("attributes.custom.attribute2.attribute-class", "com.ibm.fhir.model.type.Code"); + configuration.setProperty("attributes.custom.attribute2.serializer-class", "com.ibm.fhir.term.graph.serialize.CodeSerializer"); + configuration.setProperty("attributes.custom.attribute3.attribute-class", "com.ibm.fhir.model.type.DateTime"); + configuration.setProperty("attributes.custom.attribute3.serializer-class", "com.ibm.fhir.term.graph.serialize.DateTimeSerializer"); + configuration.setProperty("attributes.custom.attribute4.attribute-class", "com.ibm.fhir.model.type.Decimal"); + configuration.setProperty("attributes.custom.attribute4.serializer-class", "com.ibm.fhir.term.graph.serialize.DecimalSerializer"); + configuration.setProperty("attributes.custom.attribute5.attribute-class", "com.ibm.fhir.model.type.Integer"); + configuration.setProperty("attributes.custom.attribute5.serializer-class", "com.ibm.fhir.term.graph.serialize.IntegerSerializer"); + configuration.setProperty("attributes.custom.attribute6.attribute-class", "com.ibm.fhir.model.type.String"); + configuration.setProperty("attributes.custom.attribute6.serializer-class", "com.ibm.fhir.term.graph.serialize.StringSerializer"); + } + private void createSchema(JanusGraph graph) { log.info("Creating schema..."); @@ -77,8 +97,16 @@ private void createSchema(JanusGraph graph) { PropertyKey codeLowerCase = management.makePropertyKey("codeLowerCase").dataType(String.class).make(); PropertyKey display = management.makePropertyKey("display").dataType(String.class).make(); PropertyKey displayLowerCase = management.makePropertyKey("displayLowerCase").dataType(String.class).make(); - PropertyKey value = management.makePropertyKey("value").dataType(String.class).make(); PropertyKey url = management.makePropertyKey("url").dataType(String.class).make(); + PropertyKey value = management.makePropertyKey("value").dataType(String.class).make(); + + PropertyKey valueBoolean = management.makePropertyKey("valueBoolean").dataType(com.ibm.fhir.model.type.Boolean.class).make(); + PropertyKey valueCode = management.makePropertyKey("valueCode").dataType(Code.class).make(); + PropertyKey valueDateTime = management.makePropertyKey("valueDateTime").dataType(DateTime.class).make(); + PropertyKey valueDecimal = management.makePropertyKey("valueDecimal").dataType(Decimal.class).make(); + PropertyKey valueInteger = management.makePropertyKey("valueInteger").dataType(com.ibm.fhir.model.type.Integer.class).make(); + PropertyKey valueString = management.makePropertyKey("valueString").dataType(com.ibm.fhir.model.type.String.class).make(); + // property keys (not indexed) management.makePropertyKey("count").dataType(Integer.class).make(); @@ -109,6 +137,13 @@ private void createSchema(JanusGraph graph) { management.buildIndex("byUrlAndVersion", Vertex.class).addKey(url).addKey(version).buildCompositeIndex(); management.buildIndex("byValue", Vertex.class).addKey(value).buildCompositeIndex(); + management.buildIndex("byValueBoolean", Vertex.class).addKey(valueBoolean).buildCompositeIndex(); + management.buildIndex("byValueCode", Vertex.class).addKey(valueCode).buildCompositeIndex(); + management.buildIndex("byValueDateTime", Vertex.class).addKey(valueDateTime).buildCompositeIndex(); + management.buildIndex("byValueDecimal", Vertex.class).addKey(valueDecimal).buildCompositeIndex(); + management.buildIndex("byValueInteger", Vertex.class).addKey(valueInteger).buildCompositeIndex(); + management.buildIndex("byValueString", Vertex.class).addKey(valueString).buildCompositeIndex(); + // mixed indexes management.buildIndex("vertices", Vertex.class).addKey(display).addKey(value).buildMixedIndex("search"); diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/CodeSystemTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/CodeSystemTermGraphLoader.java new file mode 100644 index 00000000000..d5f46eac07e --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/CodeSystemTermGraphLoader.java @@ -0,0 +1,121 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.loader; + +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; +import static com.ibm.fhir.term.util.CodeSystemSupport.getConcepts; +import static com.ibm.fhir.term.util.CodeSystemSupport.isCaseSensitive; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.apache.tinkerpop.gremlin.structure.Vertex; + +import com.ibm.fhir.model.resource.CodeSystem; +import com.ibm.fhir.model.resource.CodeSystem.Concept; +import com.ibm.fhir.model.resource.CodeSystem.Concept.Designation; +import com.ibm.fhir.model.resource.CodeSystem.Concept.Property; +import com.ibm.fhir.model.type.Element; +import com.ibm.fhir.term.graph.FHIRTermGraph; + +public class CodeSystemTermGraphLoader { + private final FHIRTermGraph termGraph; + private final CodeSystem codeSystem; + + public CodeSystemTermGraphLoader(FHIRTermGraph termGraph, CodeSystem codeSystem) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + this.termGraph = Objects.requireNonNull(termGraph, "termGraph"); + this.codeSystem = Objects.requireNonNull(codeSystem, "codeSystem"); + } + + public void load() { + GraphTraversalSource g = termGraph.traversal(); + + Map conceptVertexMap = new HashMap<>(); + + String url = codeSystem.getUrl().getValue(); + + Vertex codeSystemVertex = g.addV("CodeSystem") + .property("url", url) + .property("designationUseSystem", url) + .property("caseSensitive", isCaseSensitive(codeSystem)) + .next(); + + if (codeSystem.getVersion() != null) { + g.V(codeSystemVertex) + .property("version", codeSystem.getVersion().getValue()) + .next(); + } + + Set concepts = getConcepts(codeSystem); + + for (Concept concept : concepts) { + String code = concept.getCode().getValue(); + Vertex conceptVertex = g.addV("Concept") + .property("code", code) + .property("codeLowerCase", normalize(code)) + .next(); + + if (concept.getDisplay() != null) { + String display = concept.getDisplay().getValue(); + g.V(conceptVertex) + .property("display", display) + .property("displayLowerCase", normalize(display)) + .next(); + } + + for (Designation designation : concept.getDesignation()) { + Vertex designationVertex = g.addV("Designation") + .property("value", designation.getValue().getValue()) + .next(); + + if (designation.getUse() != null) { + g.V(designationVertex) + .property("use", designation.getUse().getCode().getValue()) + .next(); + } + + if (designation.getLanguage() != null) { + g.V(designationVertex) + .property("language", designation.getLanguage().getValue()) + .next(); + } + + g.V(conceptVertex).addE("designation").to(designationVertex).next(); + } + + for (Property property : concept.getProperty()) { + Element value = property.getValue(); + Vertex propertyVertex = g.addV("Property_") + .property("code", property.getCode().getValue()) + .property("value" + value.getClass().getSimpleName(), value) + .next(); + + g.V(conceptVertex).addE("property_").to(propertyVertex).next(); + } + + g.V(codeSystemVertex).addE("concept").to(conceptVertex).next(); + + conceptVertexMap.put(concept, conceptVertex); + } + + for (Concept concept : concepts) { + Vertex v = conceptVertexMap.get(concept); + for (Concept child : concept.getConcept()) { + Vertex w = conceptVertexMap.get(child); + g.V(w).addE("isA").to(v).next(); + } + } + + g.tx().commit(); + + termGraph.close(); + } +} 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 4b9024b5188..4c192c85cf9 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 @@ -28,6 +28,7 @@ import com.ibm.fhir.model.resource.CodeSystem; import com.ibm.fhir.model.resource.CodeSystem.Concept; import com.ibm.fhir.model.resource.CodeSystem.Concept.Designation; +import com.ibm.fhir.model.resource.ValueSet.Compose.Include.Filter; import com.ibm.fhir.model.type.Code; import com.ibm.fhir.model.type.Coding; import com.ibm.fhir.model.type.Uri; @@ -102,6 +103,11 @@ public Set getConcepts(CodeSystem codeSystem) { return Collections.emptySet(); } + @Override + public Set getConcepts(CodeSystem codeSystem, List filters) { + throw new UnsupportedOperationException(); + } + @Override public Set closure(CodeSystem codeSystem, Code code) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/BooleanSerializer.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/BooleanSerializer.java new file mode 100644 index 00000000000..7d1312918f4 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/BooleanSerializer.java @@ -0,0 +1,25 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.serialize; + +import org.janusgraph.core.attribute.AttributeSerializer; +import org.janusgraph.diskstorage.ScanBuffer; +import org.janusgraph.diskstorage.WriteBuffer; + +import com.ibm.fhir.model.type.Boolean; + +public class BooleanSerializer implements AttributeSerializer { + @Override + public Boolean read(ScanBuffer buffer) { + return Boolean.of(buffer.getBoolean()); + } + + @Override + public void write(WriteBuffer buffer, Boolean attribute) { + buffer.putBoolean(attribute.getValue()); + } +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/CodeSerializer.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/CodeSerializer.java new file mode 100644 index 00000000000..69d870ea499 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/CodeSerializer.java @@ -0,0 +1,32 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.serialize; + +import org.janusgraph.core.attribute.AttributeSerializer; +import org.janusgraph.diskstorage.ScanBuffer; +import org.janusgraph.diskstorage.WriteBuffer; +import org.janusgraph.graphdb.database.serialize.attribute.StringSerializer; + +import com.ibm.fhir.model.type.Code; + +public class CodeSerializer implements AttributeSerializer { + private final StringSerializer serializer; + + public CodeSerializer() { + serializer = new StringSerializer(); + } + + @Override + public Code read(ScanBuffer buffer) { + return Code.of(serializer.read(buffer)); + } + + @Override + public void write(WriteBuffer buffer, Code attribute) { + serializer.write(buffer, attribute.getValue()); + } +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DateTimeSerializer.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DateTimeSerializer.java new file mode 100644 index 00000000000..6aee119c5e4 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DateTimeSerializer.java @@ -0,0 +1,31 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.serialize; + +import org.janusgraph.core.attribute.AttributeSerializer; +import org.janusgraph.diskstorage.ScanBuffer; +import org.janusgraph.diskstorage.WriteBuffer; +import org.janusgraph.graphdb.database.serialize.attribute.StringSerializer; +import com.ibm.fhir.model.type.DateTime; + +public class DateTimeSerializer implements AttributeSerializer { + private final StringSerializer serializer; + + public DateTimeSerializer() { + serializer = new StringSerializer(); + } + + @Override + public DateTime read(ScanBuffer buffer) { + return DateTime.of(serializer.read(buffer)); + } + + @Override + public void write(WriteBuffer buffer, DateTime attribute) { + serializer.write(buffer, DateTime.PARSER_FORMATTER.format(attribute.getValue())); + } +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DecimalSerializer.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DecimalSerializer.java new file mode 100644 index 00000000000..5782a278dc7 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DecimalSerializer.java @@ -0,0 +1,25 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.serialize; + +import org.janusgraph.core.attribute.AttributeSerializer; +import org.janusgraph.diskstorage.ScanBuffer; +import org.janusgraph.diskstorage.WriteBuffer; + +import com.ibm.fhir.model.type.Decimal; + +public class DecimalSerializer implements AttributeSerializer { + @Override + public Decimal read(ScanBuffer buffer) { + return Decimal.of(buffer.getDouble()); + } + + @Override + public void write(WriteBuffer buffer, Decimal attribute) { + buffer.putDouble(attribute.getValue().doubleValue()); + } +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/IntegerSerializer.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/IntegerSerializer.java new file mode 100644 index 00000000000..4747164f940 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/IntegerSerializer.java @@ -0,0 +1,25 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.serialize; + +import org.janusgraph.core.attribute.AttributeSerializer; +import org.janusgraph.diskstorage.ScanBuffer; +import org.janusgraph.diskstorage.WriteBuffer; + +import com.ibm.fhir.model.type.Integer; + +public class IntegerSerializer implements AttributeSerializer { + @Override + public Integer read(ScanBuffer buffer) { + return Integer.of(buffer.getInt()); + } + + @Override + public void write(WriteBuffer buffer, Integer attribute) { + buffer.putInt(attribute.getValue()); + } +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/StringSerializer.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/StringSerializer.java new file mode 100644 index 00000000000..f1806a914cf --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/StringSerializer.java @@ -0,0 +1,33 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.serialize; + +import static com.ibm.fhir.model.type.String.string; + +import org.janusgraph.core.attribute.AttributeSerializer; +import org.janusgraph.diskstorage.ScanBuffer; +import org.janusgraph.diskstorage.WriteBuffer; + +import com.ibm.fhir.model.type.String; + +public class StringSerializer implements AttributeSerializer { + private final org.janusgraph.graphdb.database.serialize.attribute.StringSerializer serializer; + + public StringSerializer() { + serializer = new org.janusgraph.graphdb.database.serialize.attribute.StringSerializer(); + } + + @Override + public String read(ScanBuffer buffer) { + return string(serializer.read(buffer)); + } + + @Override + public void write(WriteBuffer buffer, String attribute) { + serializer.write(buffer, attribute.getValue()); + } +} diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java new file mode 100644 index 00000000000..ee191698e93 --- /dev/null +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java @@ -0,0 +1,35 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.test; + +import java.io.InputStream; + +import org.apache.commons.configuration.PropertiesConfiguration; + +import com.ibm.fhir.model.format.Format; +import com.ibm.fhir.model.parser.FHIRParser; +import com.ibm.fhir.model.resource.CodeSystem; +import com.ibm.fhir.model.type.Code; +import com.ibm.fhir.term.graph.FHIRTermGraph; +import com.ibm.fhir.term.graph.FHIRTermGraphFactory; +import com.ibm.fhir.term.graph.loader.CodeSystemTermGraphLoader; +import com.ibm.fhir.term.graph.provider.GraphTermServiceProvider; + +public class CodeSystemTermGraphLoaderTest { + public static void main(String[] args) throws Exception { + try (InputStream in = CodeSystemTermGraphLoaderTest.class.getClassLoader().getResourceAsStream("JSON/CodeSystem-cs5.json")) { + CodeSystem codeSystem = FHIRParser.parser(Format.JSON).parse(in); + FHIRTermGraph termGraph = FHIRTermGraphFactory.open("conf/local-graph.properties"); + CodeSystemTermGraphLoader loader = new CodeSystemTermGraphLoader(termGraph, codeSystem); + loader.load(); + + GraphTermServiceProvider provider = new GraphTermServiceProvider(new PropertiesConfiguration("conf/local-graph.properties")); + System.out.println(provider.subsumes(codeSystem, Code.of("m"), Code.of("p"))); + provider.getGraph().close(); + } + } +} diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java index f6798964b94..5bb79fff9a2 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */ @@ -11,14 +11,14 @@ import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; import org.apache.tinkerpop.gremlin.structure.Vertex; -import org.testng.annotations.Test; +import com.ibm.fhir.model.type.Code; +import com.ibm.fhir.model.type.Decimal; import com.ibm.fhir.term.graph.FHIRTermGraph; import com.ibm.fhir.term.graph.FHIRTermGraphFactory; public class FHIRTermGraphTest { - @Test - public void testTermGraph() throws Exception { + public static void main(String[] args) throws Exception { FHIRTermGraph graph = FHIRTermGraphFactory.open("conf/local-graph.properties"); GraphTraversalSource g = graph.traversal(); @@ -47,6 +47,18 @@ public void testTermGraph() throws Exception { .toStream()) .forEach(System.out::println); + g.V(v1).property("valueCode", Code.of("someCode")).next(); + g.V(v1).property("valueDecimal", Decimal.of(100.0)).next(); + g.V(v1).property("valueInteger", com.ibm.fhir.model.type.Integer.of(0)).next(); + + g.tx().commit(); + + System.out.println(g.V().has("valueCode", Code.of("someCode")).hasNext()); + System.out.println(g.V().has("valueDecimal", Decimal.of(100)).hasNext()); + System.out.println(g.V().has("valueInteger", com.ibm.fhir.model.type.Integer.of(-0)).hasNext()); + + System.out.println(Integer.valueOf(0).equals(Integer.valueOf(-0))); + graph.close(); } } diff --git a/fhir-term-graph/src/test/resources/JSON/CodeSystem-cs5.json b/fhir-term-graph/src/test/resources/JSON/CodeSystem-cs5.json new file mode 100644 index 00000000000..b4bff4fccd4 --- /dev/null +++ b/fhir-term-graph/src/test/resources/JSON/CodeSystem-cs5.json @@ -0,0 +1,79 @@ +{ + "resourceType": "CodeSystem", + "id": "cs5", + "url": "http://ibm.com/fhir/CodeSystem/cs5", + "version": "1.0.0", + "status": "active", + "hierarchyMeaning": "is-a", + "content": "complete", + "property": [ + { + "code": "label", + "uri": "http://ibm.com/fhir/concept-properties#label", + "type": "string" + }, + { + "code": "property1", + "uri": "http://ibm.com/fhir/concept-properties#property1", + "type": "string" + } + ], + "concept": [ + { + "code": "m", + "display": "concept m", + "concept": [ + { + "code": "p", + "display": "concept p", + "concept": [ + { + "code": "q", + "display": "concept q" + }, + { + "code": "r", + "display": "concept r" + } + ] + } + ] + }, + { + "code": "n", + "display": "concept n", + "concept": [ + { + "code": "s", + "display": "concept s" + } + ] + }, + { + "code": "o", + "display": "concept o", + "property": [ + { + "code": "label", + "valueString": "concept label" + } + ] + }, + { + "code": "t", + "display": "concept t", + "property": [ + { + "code": "property1", + "valueString": "value1" + } + ], + "concept": [ + { + "code": "u", + "display": "concept u" + } + ] + } + ] +} \ No newline at end of file diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java b/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java index ee533956bd1..3031fa4edc4 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java @@ -26,6 +26,7 @@ import com.ibm.fhir.model.resource.ConceptMap.Group.Element; import com.ibm.fhir.model.resource.ConceptMap.Group.Element.Target; import com.ibm.fhir.model.resource.ValueSet; +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.CodeableConcept; @@ -72,6 +73,11 @@ public Set getConcepts(CodeSystem codeSystem) { return Collections.emptySet(); } + @Override + public Set getConcepts(CodeSystem codeSystem, List filters) { + return Collections.emptySet(); + } + @Override public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { return false; @@ -104,7 +110,7 @@ public static FHIRTermService getInstance() { } /** - * Indicates whether the given code system is supported + * Indicates whether the given code system is supported. * * @param codeSystem * the code system @@ -122,6 +128,20 @@ public boolean isSupported(CodeSystem codeSystem) { return false; } + /** + * Indicates whether the given code system contains a concept with the specified code. + * + * @param codeSystem + * the code system + * @param code + * the code + * @return + * true if the given code system contains a concept with the specified code, false otherwise + */ + public boolean hasConcept(CodeSystem codeSystem, Code code) { + return findProvider(codeSystem).hasConcept(codeSystem, code); + } + /** * Get the concept in the provided code system with the specified code. * @@ -149,6 +169,21 @@ public Set getConcepts(CodeSystem codeSystem) { return findProvider(codeSystem).getConcepts(codeSystem); } + /** + * Get a set containing {@link CodeSystem.Concept} instances where all structural + * hierarchies have been flattened and filtered by the given set of value set include filters. + * + * @param codeSystem + * the code system + * @param filters + * the value set include filters + * @return + * flattened / filtered list of Concept instances for the given code system + */ + public Set getConcepts(CodeSystem codeSystem, List filters) { + return findProvider(codeSystem).getConcepts(codeSystem, filters); + } + /** * Indicates whether the given value set is expandable * diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java b/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java index 52943fa20bd..24dc8467a01 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java @@ -6,10 +6,12 @@ package com.ibm.fhir.term.service.provider; +import java.util.List; import java.util.Set; import com.ibm.fhir.model.resource.CodeSystem; import com.ibm.fhir.model.resource.CodeSystem.Concept; +import com.ibm.fhir.model.resource.ValueSet.Compose.Include.Filter; import com.ibm.fhir.model.type.Code; import com.ibm.fhir.model.type.code.CodeSystemContentMode; import com.ibm.fhir.term.spi.FHIRTermServiceProvider; @@ -39,6 +41,11 @@ public Set getConcepts(CodeSystem codeSystem) { return CodeSystemSupport.getConcepts(codeSystem); } + @Override + public Set getConcepts(CodeSystem codeSystem, List filters) { + return CodeSystemSupport.getConcepts(codeSystem, filters); + } + @Override public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { Concept concept = CodeSystemSupport.findConcept(codeSystem, codeA); diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/spi/FHIRTermServiceProvider.java b/fhir-term/src/main/java/com/ibm/fhir/term/spi/FHIRTermServiceProvider.java index f82bbb2ee2e..db01f568af8 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/spi/FHIRTermServiceProvider.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/spi/FHIRTermServiceProvider.java @@ -6,15 +6,17 @@ package com.ibm.fhir.term.spi; +import java.util.List; import java.util.Set; import com.ibm.fhir.model.resource.CodeSystem; import com.ibm.fhir.model.resource.CodeSystem.Concept; +import com.ibm.fhir.model.resource.ValueSet.Compose.Include.Filter; import com.ibm.fhir.model.type.Code; public interface FHIRTermServiceProvider { /** - * Indicates whether the given code system is supported + * Indicates whether the given code system is supported. * * @param codeSystem * the code system @@ -23,7 +25,16 @@ public interface FHIRTermServiceProvider { */ boolean isSupported(CodeSystem codeSystem); - + /** + * Indicates whether the given code system contains a concept with the specified code. + * + * @param codeSystem + * the code system + * @param code + * the code + * @return + * true if the given code system contains a concept with the specified code, false otherwise + */ boolean hasConcept(CodeSystem codeSystem, Code code); /** @@ -49,6 +60,19 @@ public interface FHIRTermServiceProvider { */ Set getConcepts(CodeSystem codeSystem); + /** + * Get a set containing {@link CodeSystem.Concept} instances where all structural + * hierarchies have been flattened and filtered by the given set of value set include filters. + * + * @param codeSystem + * the code system + * @param filters + * the value set include filters + * @return + * flattened / filtered list of Concept instances for the given code system + */ + Set getConcepts(CodeSystem codeSystem, List filters); + /** * Find the concept in tree rooted by the provided concept that matches the specified code. * 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 7acd17ea4fd..1382a1a6c46 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 @@ -8,22 +8,39 @@ import static com.ibm.fhir.core.util.LRUCache.createLRUCache; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import com.ibm.fhir.model.resource.CodeSystem; import com.ibm.fhir.model.resource.CodeSystem.Concept; +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.CodeableConcept; +import com.ibm.fhir.model.type.DateTime; +import com.ibm.fhir.model.type.Decimal; import com.ibm.fhir.model.type.Element; +import com.ibm.fhir.model.type.Integer; +import com.ibm.fhir.model.type.String; +import com.ibm.fhir.model.type.code.CodeSystemHierarchyMeaning; import com.ibm.fhir.registry.FHIRRegistry; /** * A utility class for FHIR code systems */ public final class CodeSystemSupport { - private static final Map CASE_SENSITIVITY_CACHE = createLRUCache(2048); + private static final Logger log = Logger.getLogger(CodeSystemSupport.class.getName()); + + private static final Map CASE_SENSITIVITY_CACHE = createLRUCache(2048); private CodeSystemSupport() { } @@ -75,15 +92,33 @@ public static Concept findConcept(CodeSystem codeSystem, Concept concept, Code c } /** - * Indicates whether the code system with the given url is case sensitive + * Determine whether a code system property with the specified code exists in the + * provided code system. * - * @param url - * the url + * @param codeSystem + * the code system + * @param code + * the property code * @return - * true if the code system with the given is case sensitive, false otherwise + * true if the code system property exists, false otherwise */ - public static boolean isCaseSensitive(String url) { - return CASE_SENSITIVITY_CACHE.computeIfAbsent(url, k -> isCaseSensitive(getCodeSystem(url))); + 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; } /** @@ -96,11 +131,23 @@ public static boolean isCaseSensitive(String url) { */ public static boolean isCaseSensitive(CodeSystem codeSystem) { if (codeSystem != null && codeSystem.getCaseSensitive() != null) { - return Boolean.TRUE.equals(codeSystem.getCaseSensitive().getValue()); + 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))); + } + /** * Get the code system associated with the given url from the FHIR registry. * @@ -183,6 +230,28 @@ public static Set getConcepts(CodeSystem codeSystem) { return concepts; } + /** + * Get a set containing {@link CodeSystem.Concept} instances where all structural + * hierarchies have been flattened and filtered by the given set of value set include filters. + * + * @param codeSystem + * the code system + * @param filters + * the value set include filters + * @return + * flattened / filtered list of Concept instances for the given code system + */ + public static Set getConcepts(CodeSystem codeSystem, List filters) { + Set concepts = new LinkedHashSet<>(); + List conceptFilters = buildConceptFilters(codeSystem, filters); + for (Concept concept : getConcepts(codeSystem)) { + if (accept(conceptFilters, concept)) { + concepts.add(concept); + } + } + return concepts; + } + /** * Get a set containing {@link CodeSystem.Concept} instances where all structural * hierarchies have been flattened. @@ -204,33 +273,331 @@ public static Set getConcepts(Concept concept) { return concepts; } - /** - * 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; + private static boolean accept(List conceptFilters, Concept concept) { + for (ConceptFilter conceptFilter : conceptFilters) { + if (!conceptFilter.accept(concept)) { + return false; + } + } + return true; } - /** - * 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; + private static List buildConceptFilters(CodeSystem codeSystem, List filters) { + List conceptFilters = new ArrayList<>(filters.size()); + for (Filter filter : filters) { + ConceptFilter conceptFilter = null; + switch (filter.getOp().getValueAsEnumConstant()) { + case DESCENDENT_OF: + conceptFilter = createDescendentOfFilter(codeSystem, filter); + break; + case EQUALS: + conceptFilter = createEqualsFilter(codeSystem, filter); + break; + case EXISTS: + conceptFilter = createExistsFilter(codeSystem, filter); + break; + case GENERALIZES: + conceptFilter = createGeneralizesFilter(codeSystem, filter); + break; + case IN: + conceptFilter = createInFilter(codeSystem, filter); + break; + case IS_A: + conceptFilter = createIsAFilter(codeSystem, filter); + break; + case IS_NOT_A: + conceptFilter = createIsNotAFilter(codeSystem, filter); + break; + case NOT_IN: + conceptFilter = createNotInFilter(codeSystem, filter); + break; + case REGEX: + conceptFilter = createRegexFilter(codeSystem, filter); + break; + } + if (conceptFilter != null) { + conceptFilters.add(conceptFilter); + } else { + log.log(Level.WARNING, java.lang.String.format("Unable to create concept filter from property: %s, op: %s, value: %s", filter.getProperty().getValue(), filter.getOp().getValue(), filter.getValue().getValue())); + } + } + return conceptFilters; + } + + private static Code code(String value) { + return Code.of(value.getValue()); + } + + private static Element convert(String value, Class targetType) { + if (Code.class.equals(targetType)) { + return Code.of(value.getValue()); + } + if (Integer.class.equals(targetType)) { + return Integer.of(value.getValue()); + } + if (Boolean.class.equals(targetType)) { + return Boolean.of(value.getValue()); + } + if (DateTime.class.equals(targetType)) { + return DateTime.of(value.getValue()); + } + if (Decimal.class.equals(targetType)) { + return Decimal.of(value.getValue()); + } + return value; + } + + private static boolean convertsToBoolean(String value) { + return "true".equals(value.getValue()) || "false".equals(value.getValue()); + } + + private static Boolean toBoolean(String value) { + return "true".equals(value.getValue()) ? Boolean.TRUE : Boolean.FALSE; + } + + private static ConceptFilter createDescendentOfFilter(CodeSystem codeSystem, Filter filter) { + if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + Concept concept = findConcept(codeSystem, code(filter.getValue())); + if (concept != null) { + return new DescendentOfFilter(concept); + } + } + return null; + } + + private static ConceptFilter createEqualsFilter(CodeSystem codeSystem, Filter filter) { + Code property = filter.getProperty(); + if ("parent".equals(property.getValue()) || + "child".equals(property.getValue()) || + hasCodeSystemProperty(codeSystem, property)) { + return new EqualsFilter(codeSystem, property, filter.getValue()); + } + return null; + } + + private static ConceptFilter createExistsFilter(CodeSystem codeSystem, Filter filter) { + Code property = filter.getProperty(); + String value = filter.getValue(); + if (hasCodeSystemProperty(codeSystem, property) && convertsToBoolean(value)) { + return new ExistsFilter(property, toBoolean(value)); + } + return null; + } + + private static ConceptFilter createGeneralizesFilter(CodeSystem codeSystem, Filter filter) { + if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + Concept concept = findConcept(codeSystem, code(filter.getValue())); + if (concept != null) { + return new GeneralizesFilter(concept); + } + } + return null; + } + + private static ConceptFilter createInFilter(CodeSystem codeSystem, Filter filter) { + Code property = filter.getProperty(); + if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { + return new InFilter(property, Arrays.asList(filter.getValue().getValue().split(",")).stream() + .map(Code::of) + .collect(Collectors.toSet())); + } + return null; + } + + private static ConceptFilter createIsAFilter(CodeSystem codeSystem, Filter filter) { + if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + Concept concept = findConcept(codeSystem, code(filter.getValue())); + if (concept != null) { + return new IsAFilter(concept); + } + } + return null; + } + + private static ConceptFilter createIsNotAFilter(CodeSystem codeSystem, Filter filter) { + if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + Concept concept = findConcept(codeSystem, code(filter.getValue())); + if (concept != null) { + return new IsNotAFilter(concept); + } + } + return null; + } + + private static ConceptFilter createNotInFilter(CodeSystem codeSystem, Filter filter) { + Code property = filter.getProperty(); + if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { + return new NotInFilter(property, Arrays.asList(filter.getValue().getValue().split(",")).stream() + .map(Code::of) + .collect(Collectors.toSet())); + } + return null; + } + + private static ConceptFilter createRegexFilter(CodeSystem codeSystem, Filter filter) { + Code property = filter.getProperty(); + if (hasCodeSystemProperty(codeSystem, property)) { + return new RegexFilter(property, filter.getValue()); + } + return null; + } + + private interface ConceptFilter { + boolean accept(Concept concept); + } + + private static class DescendentOfFilter implements ConceptFilter { + private final Set descendants; + + public DescendentOfFilter(Concept concept) { + Set descendants = getConcepts(concept); + descendants.remove(concept); + this.descendants = descendants; + } + + @Override + public boolean accept(Concept concept) { + return descendants.contains(concept); + } + } + + private static class EqualsFilter implements ConceptFilter { + private final Code property; + private final String value; + private final Set children; + private final Concept child; + + public EqualsFilter(CodeSystem codeSystem, Code property, String value) { + this.property = property; + this.value = value; + children = new LinkedHashSet<>(); + if ("parent".equals(property.getValue())) { + Concept parent = findConcept(codeSystem, code(value)); + if (parent != null) { + children.addAll(parent.getConcept()); + } + } + this.child = "child".equals(property.getValue()) ? findConcept(codeSystem, code(value)) : null; + } + + @Override + public boolean accept(Concept concept) { + if ("parent".equals(property.getValue())) { + return children.contains(concept); + } + if ("child".equals(property.getValue())) { + return concept.getConcept().contains(child); + } + if (hasConceptProperty(concept, property)) { + Element value = getConceptPropertyValue(concept, property); + if (value != null && !value.is(CodeableConcept.class)) { + return value.equals(convert(this.value, value.getClass())); + } + } + return false; + } + } + + private static class ExistsFilter implements ConceptFilter { + private Code property; + private Boolean value; + + public ExistsFilter(Code property, Boolean value) { + this.property = property; + this.value = value; + } + + @Override + public boolean accept(Concept concept) { + return Boolean.TRUE.equals(value) ? + hasConceptProperty(concept, property) : + !hasConceptProperty(concept, property); + } + } + + private static class GeneralizesFilter implements ConceptFilter { + private final Concept concept; + + public GeneralizesFilter(Concept concept) { + this.concept = concept; + } + + @Override + public boolean accept(Concept concept) { + return getConcepts(concept).contains(this.concept); + } + } + + private static class InFilter implements ConceptFilter { + protected final Code property; + protected final Set set; + + public InFilter(Code property, Set set) { + this.property = property; + this.set = set; + } + + @Override + public boolean accept(Concept concept) { + return "concept".equals(property.getValue()) ? + set.contains(concept.getCode()) : + set.contains(getConceptPropertyValue(concept, property)); + } + } + + private static class IsAFilter implements ConceptFilter { + protected final Set descendantsAndSelf; + + public IsAFilter(Concept concept) { + descendantsAndSelf = getConcepts(concept); + } + + @Override + public boolean accept(Concept concept) { + return descendantsAndSelf.contains(concept); + } + } + + private static class IsNotAFilter extends IsAFilter { + public IsNotAFilter(Concept concept) { + super(concept); + } + + @Override + public boolean accept(Concept concept) { + return !super.accept(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; + + public RegexFilter(Code property, String value) { + this.property = property; + this.pattern = Pattern.compile(value.getValue()); + } + + @Override + public boolean accept(Concept concept) { + if (hasConceptProperty(concept, property)) { + Element value = getConceptPropertyValue(concept, property); + if (value.is(String.class)) { + return pattern.matcher(value.as(String.class).getValue()).matches(); + } + } + return false; + } } } \ No newline at end of file diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java b/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java index e7440da8cc4..e3334f2394c 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java @@ -9,14 +9,10 @@ import static com.ibm.fhir.core.util.LRUCache.createLRUCache; import static com.ibm.fhir.model.type.String.string; import static com.ibm.fhir.term.util.CodeSystemSupport.getCodeSystem; -import static com.ibm.fhir.term.util.CodeSystemSupport.getConceptPropertyValue; -import static com.ibm.fhir.term.util.CodeSystemSupport.hasCodeSystemProperty; -import static com.ibm.fhir.term.util.CodeSystemSupport.hasConceptProperty; import static com.ibm.fhir.term.util.CodeSystemSupport.isCaseSensitive; import java.time.ZoneOffset; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -26,7 +22,6 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.regex.Pattern; import java.util.stream.Collectors; import com.ibm.fhir.model.resource.CodeSystem; @@ -35,21 +30,14 @@ import com.ibm.fhir.model.resource.ValueSet; import com.ibm.fhir.model.resource.ValueSet.Compose; import com.ibm.fhir.model.resource.ValueSet.Compose.Include; -import com.ibm.fhir.model.resource.ValueSet.Compose.Include.Filter; import com.ibm.fhir.model.resource.ValueSet.Expansion; -import com.ibm.fhir.model.type.Boolean; import com.ibm.fhir.model.type.Canonical; import com.ibm.fhir.model.type.Code; -import com.ibm.fhir.model.type.CodeableConcept; import com.ibm.fhir.model.type.Coding; import com.ibm.fhir.model.type.DateTime; -import com.ibm.fhir.model.type.Decimal; -import com.ibm.fhir.model.type.Element; import com.ibm.fhir.model.type.Integer; import com.ibm.fhir.model.type.String; import com.ibm.fhir.model.type.Uri; -import com.ibm.fhir.model.type.code.CodeSystemHierarchyMeaning; -import com.ibm.fhir.model.type.code.FilterOperator; import com.ibm.fhir.registry.FHIRRegistry; import com.ibm.fhir.term.service.FHIRTermService; @@ -183,57 +171,6 @@ public static ValueSet getValueSet(java.lang.String url) { return FHIRRegistry.getInstance().getResource(url, ValueSet.class); } - private static boolean accept(List conceptFilters, Concept concept) { - for (ConceptFilter conceptFilter : conceptFilters) { - if (!conceptFilter.accept(concept)) { - return false; - } - } - return true; - } - - private static List buildConceptFilters(CodeSystem codeSystem, List filters) { - List conceptFilters = new ArrayList<>(filters.size()); - for (Filter filter : filters) { - ConceptFilter conceptFilter = null; - switch (FilterOperator.ValueSet.from(filter.getOp().getValue())) { - case DESCENDENT_OF: - conceptFilter = createDescendentOfFilter(codeSystem, filter); - break; - case EQUALS: - conceptFilter = createEqualsFilter(codeSystem, filter); - break; - case EXISTS: - conceptFilter = createExistsFilter(codeSystem, filter); - break; - case GENERALIZES: - conceptFilter = createGeneralizesFilter(codeSystem, filter); - break; - case IN: - conceptFilter = createInFilter(codeSystem, filter); - break; - case IS_A: - conceptFilter = createIsAFilter(codeSystem, filter); - break; - case IS_NOT_A: - conceptFilter = createIsNotAFilter(codeSystem, filter); - break; - case NOT_IN: - conceptFilter = createNotInFilter(codeSystem, filter); - break; - case REGEX: - conceptFilter = createRegexFilter(codeSystem, filter); - break; - } - if (conceptFilter != null) { - conceptFilters.add(conceptFilter); - } else { - log.log(Level.WARNING, java.lang.String.format("Unable to create concept filter from property: %s, op: %s, value: %s", filter.getProperty().getValue(), filter.getOp().getValue(), filter.getValue().getValue())); - } - } - return conceptFilters; - } - private static Contains buildContains(Uri system, String version, Code code, String display) { return wrap(Expansion.Contains.builder() .system(system) @@ -251,119 +188,7 @@ private static Contains buildContains(Uri system, String version, Concept concep return null; } - private static Code code(String value) { - return Code.of(value.getValue()); - } - - private static Element convert(String value, Class targetType) { - if (Code.class.equals(targetType)) { - return Code.of(value.getValue()); - } - if (Integer.class.equals(targetType)) { - return Integer.of(value.getValue()); - } - if (Boolean.class.equals(targetType)) { - return Boolean.of(value.getValue()); - } - if (DateTime.class.equals(targetType)) { - return DateTime.of(value.getValue()); - } - if (Decimal.class.equals(targetType)) { - return Decimal.of(value.getValue()); - } - return value; - } - - private static boolean convertsToBoolean(String value) { - return "true".equals(value.getValue()) || "false".equals(value.getValue()); - } - - private static ConceptFilter createDescendentOfFilter(CodeSystem codeSystem, Filter filter) { - if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - Concept concept = FHIRTermService.getInstance().getConcept(codeSystem, code(filter.getValue())); - if (concept != null) { - return new DescendentOfFilter(codeSystem, concept); - } - } - return null; - } - - private static ConceptFilter createEqualsFilter(CodeSystem codeSystem, Filter filter) { - Code property = filter.getProperty(); - if ("parent".equals(property.getValue()) || - "child".equals(property.getValue()) || - hasCodeSystemProperty(codeSystem, property)) { - return new EqualsFilter(codeSystem, property, filter.getValue()); - } - return null; - } - private static ConceptFilter createExistsFilter(CodeSystem codeSystem, Filter filter) { - Code property = filter.getProperty(); - String value = filter.getValue(); - if (hasCodeSystemProperty(codeSystem, property) && convertsToBoolean(value)) { - return new ExistsFilter(property, toBoolean(value)); - } - return null; - } - - private static ConceptFilter createGeneralizesFilter(CodeSystem codeSystem, Filter filter) { - if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - Concept concept = FHIRTermService.getInstance().getConcept(codeSystem, code(filter.getValue())); - if (concept != null) { - return new GeneralizesFilter(codeSystem, concept); - } - } - return null; - } - - private static ConceptFilter createInFilter(CodeSystem codeSystem, Filter filter) { - Code property = filter.getProperty(); - if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { - return new InFilter(property, Arrays.asList(filter.getValue().getValue().split(",")).stream() - .map(Code::of) - .collect(Collectors.toSet())); - } - return null; - } - - private static ConceptFilter createIsAFilter(CodeSystem codeSystem, Filter filter) { - if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - Concept concept = FHIRTermService.getInstance().getConcept(codeSystem, code(filter.getValue())); - if (concept != null) { - return new IsAFilter(codeSystem, concept); - } - } - return null; - } - - private static ConceptFilter createIsNotAFilter(CodeSystem codeSystem, Filter filter) { - if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - Concept concept = FHIRTermService.getInstance().getConcept(codeSystem, code(filter.getValue())); - if (concept != null) { - return new IsNotAFilter(codeSystem, concept); - } - } - return null; - } - - private static ConceptFilter createNotInFilter(CodeSystem codeSystem, Filter filter) { - Code property = filter.getProperty(); - if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { - return new NotInFilter(property, Arrays.asList(filter.getValue().getValue().split(",")).stream() - .map(Code::of) - .collect(Collectors.toSet())); - } - return null; - } - - private static ConceptFilter createRegexFilter(CodeSystem codeSystem, Filter filter) { - Code property = filter.getProperty(); - if (hasCodeSystemProperty(codeSystem, property)) { - return new RegexFilter(property, filter.getValue()); - } - return null; - } private static Set expand(Compose compose) { if (compose == null) { @@ -411,15 +236,12 @@ private static Set expand(Include includeOrExclude) { if (version != null) { url = url + "|" + version.getValue(); } - if (hasResource(url, CodeSystem.class)) { - CodeSystem codeSystem = getCodeSystem(url); - List conceptFilters = buildConceptFilters(codeSystem, includeOrExclude.getFilter()); - for (Concept concept : FHIRTermService.getInstance().getConcepts(codeSystem)) { - if (accept(conceptFilters, concept)) { - Contains contains = buildContains(system, version, concept); - if (contains != null) { - systemContains.add(contains); - } + CodeSystem codeSystem = getCodeSystem(url); + if (codeSystem != null) { + for (Concept concept : FHIRTermService.getInstance().getConcepts(codeSystem, includeOrExclude.getFilter())) { + Contains contains = buildContains(system, version, concept); + if (contains != null) { + systemContains.add(contains); } } } @@ -464,169 +286,6 @@ private static boolean hasResource(java.lang.String url, Class children; - private final Concept child; - - public EqualsFilter(CodeSystem codeSystem, Code property, String value) { - this.property = property; - this.value = value; - children = new LinkedHashSet<>(); - if ("parent".equals(property.getValue())) { - Concept parent = FHIRTermService.getInstance().getConcept(codeSystem, code(value)); - if (parent != null) { - children.addAll(parent.getConcept()); - } - } - this.child = "child".equals(property.getValue()) ? FHIRTermService.getInstance().getConcept(codeSystem, code(value)) : null; - } - - @Override - public boolean accept(Concept concept) { - if ("parent".equals(property.getValue())) { - return children.contains(concept); - } - if ("child".equals(property.getValue())) { - return concept.getConcept().contains(child); - } - if (hasConceptProperty(concept, property)) { - Element value = getConceptPropertyValue(concept, property); - if (value != null && !value.is(CodeableConcept.class)) { - return value.equals(convert(this.value, value.getClass())); - } - } - return false; - } - } - - private static class ExistsFilter implements ConceptFilter { - private Code property; - private Boolean value; - - public ExistsFilter(Code property, Boolean value) { - this.property = property; - this.value = value; - } - - @Override - public boolean accept(Concept concept) { - return Boolean.TRUE.equals(value) ? - hasConceptProperty(concept, property) : - !hasConceptProperty(concept, property); - } - } - - private static class GeneralizesFilter implements ConceptFilter { - private final CodeSystem codeSystem; - private final Concept concept; - - public GeneralizesFilter(CodeSystem codeSystem, Concept concept) { - this.codeSystem = codeSystem; - this.concept = concept; - } - - @Override - public boolean accept(Concept concept) { - return FHIRTermService.getInstance().subsumes(codeSystem, concept.getCode(), this.concept.getCode()); - } - } - - private static class InFilter implements ConceptFilter { - protected final Code property; - protected final Set set; - - public InFilter(Code property, Set set) { - this.property = property; - this.set = set; - } - - @Override - public boolean accept(Concept concept) { - return "concept".equals(property.getValue()) ? - set.contains(concept.getCode()) : - set.contains(getConceptPropertyValue(concept, property)); - } - } - - private static class IsAFilter implements ConceptFilter { - protected final CodeSystem codeSystem; - protected final Concept concept; - - public IsAFilter(CodeSystem codeSystem, Concept concept) { - this.codeSystem = codeSystem; - this.concept = concept; - } - - @Override - public boolean accept(Concept concept) { - return FHIRTermService.getInstance().subsumes(codeSystem, this.concept.getCode(), concept.getCode()); - } - } - - private static class IsNotAFilter extends IsAFilter { - public IsNotAFilter(CodeSystem codeSystem, Concept concept) { - super(codeSystem, concept); - } - - @Override - public boolean accept(Concept concept) { - return !super.accept(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; - - public RegexFilter(Code property, String value) { - this.property = property; - this.pattern = Pattern.compile(value.getValue()); - } - - @Override - public boolean accept(Concept concept) { - if (hasConceptProperty(concept, property)) { - Element value = getConceptPropertyValue(concept, property); - if (value.is(String.class)) { - return pattern.matcher(value.as(String.class).getValue()).matches(); - } - } - return false; - } - } - private static Contains wrap(Expansion.Contains contains) { return new Contains(contains); } From bb3aa9fd298fccb197cd7861befff7207f1f93d2 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 2 Mar 2021 15:20:08 -0500 Subject: [PATCH 05/50] Issue #1980 - removed unused import Signed-off-by: John T.E. Timm --- .../com/ibm/fhir/server/listener/FHIRServletContextListener.java | 1 - 1 file changed, 1 deletion(-) diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java b/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java index 5a59a7b8fe2..8fcb9b8aaee 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java @@ -10,7 +10,6 @@ import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_EXTENDED_CODEABLE_CONCEPT_VALIDATION; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_CONFIGURATION; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_ENABLED; -import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_JDBC_BOOTSTRAP_DB; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_KAFKA_CONNECTIONPROPS; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_KAFKA_ENABLED; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_KAFKA_TOPICNAME; From a7e76a29cb3dbf25f1e35fbf6fd44dfb5c6c95a3 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 2 Mar 2021 15:59:33 -0500 Subject: [PATCH 06/50] Issue #1980 - add property support to GraphTermServiceProvider Signed-off-by: John T.E. Timm --- .../provider/GraphTermServiceProvider.java | 47 +++++++++++++++---- .../test/CodeSystemTermGraphLoaderTest.java | 1 + 2 files changed, 40 insertions(+), 8 deletions(-) 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 4c192c85cf9..4a1d31fe44e 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 @@ -28,9 +28,11 @@ import com.ibm.fhir.model.resource.CodeSystem; import com.ibm.fhir.model.resource.CodeSystem.Concept; import com.ibm.fhir.model.resource.CodeSystem.Concept.Designation; +import com.ibm.fhir.model.resource.CodeSystem.Concept.Property; import com.ibm.fhir.model.resource.ValueSet.Compose.Include.Filter; import com.ibm.fhir.model.type.Code; import com.ibm.fhir.model.type.Coding; +import com.ibm.fhir.model.type.Element; import com.ibm.fhir.model.type.Uri; import com.ibm.fhir.term.graph.FHIRTermGraph; import com.ibm.fhir.term.graph.FHIRTermGraphFactory; @@ -63,10 +65,10 @@ public boolean hasConcept(CodeSystem codeSystem, Code code) { @Override public Concept getConcept(CodeSystem codeSystem, Code code) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); - return getConcept(codeSystem, code, true); + return getConcept(codeSystem, code, true, true); } - private Concept getConcept(CodeSystem codeSystem, Code code, boolean includeDesignations) { + private Concept getConcept(CodeSystem codeSystem, Code code, boolean includeDesignations, boolean includeProperties) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); return createConcept( codeSystem, @@ -74,7 +76,8 @@ private Concept getConcept(CodeSystem codeSystem, Code code, boolean includeDesi whereCodeSystem(hasCode(g.V(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem) .elementMap() .tryNext(), - includeDesignations); + includeDesignations, + includeProperties); } @Override @@ -112,7 +115,7 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { public Set closure(CodeSystem codeSystem, Code code) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); Set concepts = new LinkedHashSet<>(); - concepts.add(getConcept(codeSystem, code, false)); + concepts.add(getConcept(codeSystem, code, false, false)); whereCodeSystem(hasCode(g.V(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem) .repeat(__.in("isA") .simplePath() @@ -164,22 +167,23 @@ private GraphTraversal whereCodeSystem(GraphTraversal> optional, boolean includeDesignations) { + private Concept createConcept(CodeSystem codeSystem, String code, Optional> optional, boolean includeDesignations, boolean includeProperties) { if (optional.isPresent()) { - return createConcept(optional.get(), includeDesignations ? getDesignations(codeSystem, code) : Collections.emptyList()); + return createConcept(optional.get(), includeDesignations ? getDesignations(codeSystem, code) : Collections.emptyList(), includeProperties ? getProperties(codeSystem, code) : Collections.emptyList()); } return null; } private Concept createConcept(Map elementMap) { - return createConcept(elementMap, Collections.emptyList()); + return createConcept(elementMap, Collections.emptyList(), Collections.emptyList()); } - private Concept createConcept(Map elementMap, List designations) { + private Concept createConcept(Map elementMap, List designations, List properties) { return Concept.builder() .code(Code.of((String) elementMap.get("code"))) .display(string((String) elementMap.get("display"))) .designation(designations) + .property(properties) .build(); } @@ -210,6 +214,33 @@ private List getDesignations(CodeSystem codeSystem, String code) { return designations; } + private List getProperties(CodeSystem codeSystem, String code) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + List properties = new ArrayList<>(); + whereCodeSystem(hasCode(g.V(), code, isCaseSensitive(codeSystem)), codeSystem) + .out("property_") + .elementMap() + .toStream() + .forEach(elementMap -> properties.add(createProperty(elementMap))); + return properties; + } + + private Property createProperty(Map elementMap) { + return Property.builder() + .code(Code.of((String) elementMap.get("code"))) + .value(getElement(elementMap)) + .build(); + } + + private Element getElement(Map elementMap) { + for (Object value : elementMap.values()) { + if (value instanceof Element) { + return (Element) value; + } + } + return null; + } + private String getDesignationUseSystem(CodeSystem codeSystem) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); Optional> optional = hasVersion(hasUrl(g.V(), codeSystem.getUrl()), codeSystem.getVersion()) diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java index ee191698e93..0f7dd1883ca 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java @@ -29,6 +29,7 @@ public static void main(String[] args) throws Exception { GraphTermServiceProvider provider = new GraphTermServiceProvider(new PropertiesConfiguration("conf/local-graph.properties")); System.out.println(provider.subsumes(codeSystem, Code.of("m"), Code.of("p"))); + System.out.println(provider.getConcept(codeSystem, Code.of("o"))); provider.getGraph().close(); } } From 08c8810a025d401d395dfcad545c57c270a662b9 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Wed, 3 Mar 2021 08:21:03 -0500 Subject: [PATCH 07/50] Issue #1980 - sort members Signed-off-by: John T.E. Timm --- .../provider/GraphTermServiceProvider.java | 244 +++--- .../fhir/term/service/FHIRTermService.java | 718 +++++++++--------- .../provider/DefaultTermServiceProvider.java | 26 +- .../term/spi/FHIRTermServiceProvider.java | 54 +- 4 files changed, 534 insertions(+), 508 deletions(-) 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 4a1d31fe44e..25ed71e1aa8 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 @@ -21,7 +21,6 @@ import org.apache.commons.configuration.Configuration; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; -import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; import org.apache.tinkerpop.gremlin.structure.Vertex; @@ -40,26 +39,25 @@ public class GraphTermServiceProvider implements FHIRTermServiceProvider { private final FHIRTermGraph graph; - private final GraphTraversalSource g; public GraphTermServiceProvider(Configuration configuration) { graph = FHIRTermGraphFactory.open(configuration); - g = graph.traversal(); - } - - public FHIRTermGraph getGraph() { - return graph; } @Override - public boolean isSupported(CodeSystem codeSystem) { + public Set closure(CodeSystem codeSystem, Code code) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); - return hasVersion(hasUrl(g.V(), codeSystem.getUrl()), codeSystem.getVersion()).hasNext(); - } - - @Override - public boolean hasConcept(CodeSystem codeSystem, Code code) { - return whereCodeSystem(hasCode(g.V(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem).hasNext(); + Set concepts = new LinkedHashSet<>(); + concepts.add(getConcept(codeSystem, code, false, false)); + whereCodeSystem(hasCode(vertices(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem) + .repeat(__.in("isA") + .simplePath() + .dedup()) + .emit() + .elementMap() + .toStream() + .forEach(elementMap -> concepts.add(createConcept(elementMap))); + return concepts; } @Override @@ -68,37 +66,11 @@ public Concept getConcept(CodeSystem codeSystem, Code code) { return getConcept(codeSystem, code, true, true); } - private Concept getConcept(CodeSystem codeSystem, Code code, boolean includeDesignations, boolean includeProperties) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); - return createConcept( - codeSystem, - code.getValue(), - whereCodeSystem(hasCode(g.V(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem) - .elementMap() - .tryNext(), - includeDesignations, - includeProperties); - } - - @Override - public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); - boolean caseSensitive = isCaseSensitive(codeSystem); - if (codeA.equals(codeB) || (!caseSensitive && normalize(codeA.getValue()).equals(normalize(codeB.getValue())))) { - return true; - } - return whereCodeSystem(hasCode(g.V(), codeA.getValue(), caseSensitive), codeSystem) - .repeat(__.in("isA") - .simplePath()) - .until(hasCode(codeB.getValue(), caseSensitive)) - .hasNext(); - } - @Override public Set getConcepts(CodeSystem codeSystem) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); List concepts = new ArrayList<>(getCount(codeSystem)); - hasVersion(hasUrl(g.V(), codeSystem.getUrl()), codeSystem.getVersion()) + hasVersion(hasUrl(vertices(), codeSystem.getUrl()), codeSystem.getVersion()) .out("concept") .elementMap() .toStream() @@ -108,63 +80,61 @@ public Set getConcepts(CodeSystem codeSystem) { @Override public Set getConcepts(CodeSystem codeSystem, List filters) { - throw new UnsupportedOperationException(); - } - - @Override - public Set closure(CodeSystem codeSystem, Code code) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); - Set concepts = new LinkedHashSet<>(); - concepts.add(getConcept(codeSystem, code, false, false)); - whereCodeSystem(hasCode(g.V(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem) - .repeat(__.in("isA") - .simplePath() - .dedup()) - .emit() - .elementMap() - .toStream() - .forEach(elementMap -> concepts.add(createConcept(elementMap))); - return concepts; - } - - private int getCount(CodeSystem codeSystem) { - Optional> optional = hasVersion(hasUrl(g.V(), codeSystem.getUrl()), codeSystem.getVersion()) - .elementMap("count") - .tryNext(); - if (optional.isPresent()) { - return (Integer) optional.get().get("count"); + GraphTraversal g = vertices(); + for (Filter filter : filters) { + switch (filter.getOp().getValueAsEnumConstant()) { + case DESCENDENT_OF: + break; + case EQUALS: + break; + case EXISTS: + break; + case GENERALIZES: + break; + case IN: + break; + case IS_A: + break; + case IS_NOT_A: + break; + case NOT_IN: + break; + case REGEX: + break; + default: + break; + } } - return -1; + throw new UnsupportedOperationException(); } - // anonymous graph traversal - private GraphTraversal hasCode(String code, boolean caseSensitive) { - if (caseSensitive) { - return __.has("code", code); - } - return __.has("codeLowerCase", normalize(code)); + public FHIRTermGraph getGraph() { + return graph; } - private GraphTraversal hasCode(GraphTraversal g, String code, boolean caseSensitive) { - if (caseSensitive) { - return g.has("code", code); - } - return g.has("codeLowerCase", normalize(code)); + @Override + public boolean hasConcept(CodeSystem codeSystem, Code code) { + return whereCodeSystem(hasCode(vertices(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem).hasNext(); } - private GraphTraversal hasUrl(GraphTraversal g, Uri url) { - return g.has("url", url.getValue()); + @Override + public boolean isSupported(CodeSystem codeSystem) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + return hasVersion(hasUrl(vertices(), codeSystem.getUrl()), codeSystem.getVersion()).hasNext(); } - private GraphTraversal hasVersion(GraphTraversal g, com.ibm.fhir.model.type.String version) { - if (version != null) { - return g.has("version", version.getValue()); + @Override + public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + boolean caseSensitive = isCaseSensitive(codeSystem); + if (codeA.equals(codeB) || (!caseSensitive && normalize(codeA.getValue()).equals(normalize(codeB.getValue())))) { + return true; } - return g; - } - - private GraphTraversal whereCodeSystem(GraphTraversal g, CodeSystem codeSystem) { - return g.where(hasVersion(hasUrl(__.in("concept").hasLabel("CodeSystem"), codeSystem.getUrl()), codeSystem.getVersion())); + return whereCodeSystem(hasCode(vertices(), codeA.getValue(), caseSensitive), codeSystem) + .repeat(__.in("isA") + .simplePath()) + .until(hasCode(codeB.getValue(), caseSensitive)) + .hasNext(); } private Concept createConcept(CodeSystem codeSystem, String code, Optional> optional, boolean includeDesignations, boolean includeProperties) { @@ -202,11 +172,40 @@ private Designation createDesignation(Map elementMap, String des return builder.build(); } + private Property createProperty(Map elementMap) { + return Property.builder() + .code(Code.of((String) elementMap.get("code"))) + .value(getElement(elementMap)) + .build(); + } + + private Concept getConcept(CodeSystem codeSystem, Code code, boolean includeDesignations, boolean includeProperties) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + return createConcept( + codeSystem, + code.getValue(), + whereCodeSystem(hasCode(vertices(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem) + .elementMap() + .tryNext(), + includeDesignations, + includeProperties); + } + + private int getCount(CodeSystem codeSystem) { + Optional> optional = hasVersion(hasUrl(vertices(), codeSystem.getUrl()), codeSystem.getVersion()) + .elementMap("count") + .tryNext(); + if (optional.isPresent()) { + return (Integer) optional.get().get("count"); + } + return -1; + } + private List getDesignations(CodeSystem codeSystem, String code) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); List designations = new ArrayList<>(); String designationUseSystem = getDesignationUseSystem(codeSystem); - whereCodeSystem(hasCode(g.V(), code, isCaseSensitive(codeSystem)), codeSystem) + whereCodeSystem(hasCode(vertices(), code, isCaseSensitive(codeSystem)), codeSystem) .out("designation") .elementMap() .toStream() @@ -214,10 +213,30 @@ private List getDesignations(CodeSystem codeSystem, String code) { return designations; } + private String getDesignationUseSystem(CodeSystem codeSystem) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + Optional> optional = hasVersion(hasUrl(vertices(), codeSystem.getUrl()), codeSystem.getVersion()) + .elementMap("designationUseSystem") + .tryNext(); + if (optional.isPresent()) { + return (String) optional.get().get("designationUseSystem"); + } + return null; + } + + private Element getElement(Map elementMap) { + for (Object value : elementMap.values()) { + if (value instanceof Element) { + return (Element) value; + } + } + return null; + } + private List getProperties(CodeSystem codeSystem, String code) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); List properties = new ArrayList<>(); - whereCodeSystem(hasCode(g.V(), code, isCaseSensitive(codeSystem)), codeSystem) + whereCodeSystem(hasCode(vertices(), code, isCaseSensitive(codeSystem)), codeSystem) .out("property_") .elementMap() .toStream() @@ -225,30 +244,37 @@ private List getProperties(CodeSystem codeSystem, String code) { return properties; } - private Property createProperty(Map elementMap) { - return Property.builder() - .code(Code.of((String) elementMap.get("code"))) - .value(getElement(elementMap)) - .build(); + private GraphTraversal hasCode(GraphTraversal g, String code, boolean caseSensitive) { + if (caseSensitive) { + return g.has("code", code); + } + return g.has("codeLowerCase", normalize(code)); } - private Element getElement(Map elementMap) { - for (Object value : elementMap.values()) { - if (value instanceof Element) { - return (Element) value; - } + // anonymous graph traversal + private GraphTraversal hasCode(String code, boolean caseSensitive) { + if (caseSensitive) { + return __.has("code", code); } - return null; + return __.has("codeLowerCase", normalize(code)); } - private String getDesignationUseSystem(CodeSystem codeSystem) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); - Optional> optional = hasVersion(hasUrl(g.V(), codeSystem.getUrl()), codeSystem.getVersion()) - .elementMap("designationUseSystem") - .tryNext(); - if (optional.isPresent()) { - return (String) optional.get().get("designationUseSystem"); + private GraphTraversal hasUrl(GraphTraversal g, Uri url) { + return g.has("url", url.getValue()); + } + + private GraphTraversal hasVersion(GraphTraversal g, com.ibm.fhir.model.type.String version) { + if (version != null) { + return g.has("version", version.getValue()); } - return null; + return g; + } + + private GraphTraversal vertices(Object... vertexIds) { + return graph.traversal().V(vertexIds); + } + + private GraphTraversal whereCodeSystem(GraphTraversal g, CodeSystem codeSystem) { + return g.where(hasVersion(hasUrl(__.in("concept").hasLabel("CodeSystem"), codeSystem.getUrl()), codeSystem.getVersion())); } } diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java b/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java index 3031fa4edc4..cc1248ebcd9 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java @@ -54,13 +54,8 @@ public class FHIRTermService { private static final FHIRTermService INSTANCE = new FHIRTermService(); private static final FHIRTermServiceProvider NULL_TERM_SERVICE_PROVIDER = new FHIRTermServiceProvider() { @Override - public boolean isSupported(CodeSystem codeSystem) { - return false; - } - - @Override - public boolean hasConcept(CodeSystem codeSystem, Code code) { - return false; + public Set closure(CodeSystem codeSystem, Code code) { + return Collections.emptySet(); } @Override @@ -79,13 +74,18 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { } @Override - public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { + public boolean hasConcept(CodeSystem codeSystem, Code code) { return false; } @Override - public Set closure(CodeSystem codeSystem, Code code) { - return Collections.emptySet(); + public boolean isSupported(CodeSystem codeSystem) { + return false; + } + + @Override + public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { + return false; } }; private final List providers; @@ -105,41 +105,64 @@ public void addProvider(FHIRTermServiceProvider provider) { providers.add(provider); } - public static FHIRTermService getInstance() { - return INSTANCE; + public Set closure(CodeSystem codeSystem, Code code) { + return findProvider(codeSystem).closure(codeSystem, code); } /** - * Indicates whether the given code system is supported. + * Generate the transitive closure for the code system concept represented by the given coding * - * @param codeSystem - * the code system + * @param coding + * the coding * @return - * true if the given code system is supported, false otherwise + * a set containing the transitive closure for the code system concept represented by the given coding */ - public boolean isSupported(CodeSystem codeSystem) { - if (codeSystem != null) { - for (FHIRTermServiceProvider provider : providers) { - if (provider.isSupported(codeSystem)) { - return true; + public Set closure(Coding coding) { + Uri system = coding.getSystem(); + java.lang.String version = (coding.getVersion() != null) ? coding.getVersion().getValue() : null; + Code code = coding.getCode(); + + if (system != null && code != null) { + java.lang.String url = (version != null) ? system.getValue() + "|" + version : system.getValue(); + CodeSystem codeSystem = CodeSystemSupport.getCodeSystem(url); + if (codeSystem != null && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + FHIRTermServiceProvider provider = findProvider(codeSystem); + if (provider.hasConcept(codeSystem, code)) { + return provider.closure(codeSystem, code); } } } - return false; + + return Collections.emptySet(); } /** - * Indicates whether the given code system contains a concept with the specified code. + * Expand the given value set * - * @param codeSystem - * the code system - * @param code - * the code + * @param valueSet + * the value set to expand * @return - * true if the given code system contains a concept with the specified code, false otherwise + * the expanded value set, or the original value set if already expanded or unable to expand */ - public boolean hasConcept(CodeSystem codeSystem, Code code) { - return findProvider(codeSystem).hasConcept(codeSystem, code); + public ValueSet expand(ValueSet valueSet) { + return ValueSetSupport.expand(valueSet); + } + + /** + * Expand the given value set and expansion parameters + * + * @param valueSet + * the value set to expand + * @param parameters + * the expansion parameters + * @return + * the expanded value set, or the original value set if already expanded or unable to expand + */ + public ValueSet expand(ValueSet valueSet, ExpansionParameters parameters) { + if (!ExpansionParameters.EMPTY.equals(parameters)) { + throw new UnsupportedOperationException("Expansion parameters are not supported"); + } + return ValueSetSupport.expand(valueSet); } /** @@ -185,86 +208,60 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { } /** - * Indicates whether the given value set is expandable - * - * @param valueSet - * the value set - * @return - * true if the given value set is expandable, false otherwise - */ - public boolean isExpandable(ValueSet valueSet) { - return ValueSetSupport.isExpandable(valueSet); - } - - /** - * Expand the given value set and expansion parameters + * Indicates whether the given code system contains a concept with the specified code. * - * @param valueSet - * the value set to expand - * @param parameters - * the expansion parameters + * @param codeSystem + * the code system + * @param code + * the code * @return - * the expanded value set, or the original value set if already expanded or unable to expand + * true if the given code system contains a concept with the specified code, false otherwise */ - public ValueSet expand(ValueSet valueSet, ExpansionParameters parameters) { - if (!ExpansionParameters.EMPTY.equals(parameters)) { - throw new UnsupportedOperationException("Expansion parameters are not supported"); - } - return ValueSetSupport.expand(valueSet); + public boolean hasConcept(CodeSystem codeSystem, Code code) { + return findProvider(codeSystem).hasConcept(codeSystem, code); } /** - * Expand the given value set + * Indicates whether the given value set is expandable * * @param valueSet - * the value set to expand + * the value set * @return - * the expanded value set, or the original value set if already expanded or unable to expand + * true if the given value set is expandable, false otherwise */ - public ValueSet expand(ValueSet valueSet) { - return ValueSetSupport.expand(valueSet); + public boolean isExpandable(ValueSet valueSet) { + return ValueSetSupport.isExpandable(valueSet); } /** - * Lookup the code system concept for the given system, version, code and lookup parameters + * Indicates whether the given code system is supported. * - * @param system - * the system - * @param version - * the version - * @param code - * the code - * @param parameters - * the lookup parameters + * @param codeSystem + * the code system * @return - * the outcome of the lookup + * true if the given code system is supported, false otherwise */ - public LookupOutcome lookup(Uri system, String version, Code code, LookupParameters parameters) { - if (!LookupParameters.EMPTY.equals(parameters)) { - throw new UnsupportedOperationException("Lookup parameters are not suppored"); + public boolean isSupported(CodeSystem codeSystem) { + if (codeSystem != null) { + for (FHIRTermServiceProvider provider : providers) { + if (provider.isSupported(codeSystem)) { + return true; + } + } } - Coding coding = Coding.builder() - .system(system) - .version(version) - .code(code) - .build(); - return lookup(coding, parameters); + return false; } /** - * Lookup the code system concept for the given system, version, and code + * Lookup the code system concept for the given coding * - * @param system - * the system - * @param version - * the version - * @param code - * the code + * @param coding + * the coding to lookup * @return * the outcome of the lookup */ - public LookupOutcome lookup(Uri system, String version, Code code) { - return lookup(system, version, code, LookupParameters.EMPTY); + public LookupOutcome lookup(Coding coding) { + return lookup(coding, LookupParameters.EMPTY); } /** @@ -315,15 +312,45 @@ public LookupOutcome lookup(Coding coding, LookupParameters parameters) { } /** - * Lookup the code system concept for the given coding + * Lookup the code system concept for the given system, version, and code * - * @param coding - * the coding to lookup + * @param system + * the system + * @param version + * the version + * @param code + * the code * @return * the outcome of the lookup */ - public LookupOutcome lookup(Coding coding) { - return lookup(coding, LookupParameters.EMPTY); + public LookupOutcome lookup(Uri system, String version, Code code) { + return lookup(system, version, code, LookupParameters.EMPTY); + } + + /** + * Lookup the code system concept for the given system, version, code and lookup parameters + * + * @param system + * the system + * @param version + * the version + * @param code + * the code + * @param parameters + * the lookup parameters + * @return + * the outcome of the lookup + */ + public LookupOutcome lookup(Uri system, String version, Code code, LookupParameters parameters) { + if (!LookupParameters.EMPTY.equals(parameters)) { + throw new UnsupportedOperationException("Lookup parameters are not suppored"); + } + Coding coding = Coding.builder() + .system(system) + .version(version) + .code(code) + .build(); + return lookup(coding, parameters); } /** @@ -390,115 +417,169 @@ public ConceptSubsumptionOutcome subsumes(Coding codingA, Coding codingB) { return null; } - public Set closure(CodeSystem codeSystem, Code code) { - return findProvider(codeSystem).closure(codeSystem, code); - } - /** - * Generate the transitive closure for the code system concept represented by the given coding + * Translate the given coding using the provided concept map * - * @param coding - * the coding + * @param conceptMap + * the concept map + * @param codeable concept + * the codeable concept * @return - * a set containing the transitive closure for the code system concept represented by the given coding + * the outcome of translation */ - public Set closure(Coding coding) { - Uri system = coding.getSystem(); - java.lang.String version = (coding.getVersion() != null) ? coding.getVersion().getValue() : null; - Code code = coding.getCode(); - - if (system != null && code != null) { - java.lang.String url = (version != null) ? system.getValue() + "|" + version : system.getValue(); - CodeSystem codeSystem = CodeSystemSupport.getCodeSystem(url); - if (codeSystem != null && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - FHIRTermServiceProvider provider = findProvider(codeSystem); - if (provider.hasConcept(codeSystem, code)) { - return provider.closure(codeSystem, code); - } - } - } - - return Collections.emptySet(); + public TranslationOutcome translate(ConceptMap conceptMap, CodeableConcept codeableConcept) { + return translate(conceptMap, codeableConcept, TranslationParameters.EMPTY); } /** - * Validate a code and display using the provided code system, version and validation parameters + * Translate the given codeable concept using the provided concept map and translation parameters * - * @param codeSystem - * the code system - * @param version - * the version - * @param code - * the code - * @param display - * the display + * @param conceptMap + * the concept map + * @param codeableConcept + * the codeable concept * @param parameters - * the validation parameters + * the translation parameters * @return - * the outcome of validation + * the outcome of translation */ - public ValidationOutcome validateCode(CodeSystem codeSystem, String version, Code code, String display, ValidationParameters parameters) { - if (!ValidationParameters.EMPTY.equals(parameters)) { - throw new UnsupportedOperationException("Validation parameters are not supported"); + public TranslationOutcome translate(ConceptMap conceptMap, CodeableConcept codeableConcept, TranslationParameters parameters) { + if (!TranslationParameters.EMPTY.equals(parameters)) { + throw new UnsupportedOperationException("Translation parameters are not supported"); } - Coding coding = Coding.builder() - .version(version) - .code(code) - .display(display) + for (Coding coding : codeableConcept.getCoding()) { + TranslationOutcome outcome = translate(conceptMap, coding, parameters); + if (Boolean.TRUE.equals(outcome.getResult())) { + return outcome; + } + } + return TranslationOutcome.builder() + .result(Boolean.FALSE) + .message(string("No matches found")) .build(); - return validateCode(codeSystem, coding, parameters); } /** - * Validate a code and display using the provided code system and version + * Translate the given coding using the provided concept map * - * @param code system - * the code system + * @param conceptMap + * the concept map + * @param coding + * the coding + * @return + * the outcome of translation + */ + public TranslationOutcome translate(ConceptMap conceptMap, Coding coding) { + return translate(conceptMap, coding, TranslationParameters.EMPTY); + } + + /** + * Translate the given coding using the provided concept map and translation parameters + * + * @param conceptMap + * the concept map + * @param coding + * the coding + * @param parameters + * the translation parameters + * @return + * the outcome of translation + */ + public TranslationOutcome translate(ConceptMap conceptMap, Coding coding, TranslationParameters parameters) { + if (!TranslationParameters.EMPTY.equals(parameters)) { + throw new UnsupportedOperationException("Translation parameters are not supported"); + } + Uri source = getSource(conceptMap); + List match = new ArrayList<>(); + for (Group group : conceptMap.getGroup()) { + if (group.getSource() == null || !group.getSource().equals(coding.getSystem())) { + continue; + } + if (group.getSourceVersion() != null && coding.getVersion() != null && !group.getSourceVersion().equals(coding.getVersion())) { + continue; + } + for (Element element : group.getElement()) { + if (element.getCode() == null || !element.getCode().equals(coding.getCode())) { + // TODO: handle unmatched codes here + continue; + } + for (Target target : element.getTarget()) { + match.add(Match.builder() + .equivalence(target.getEquivalence()) + .concept(Coding.builder() + .system(group.getTarget()) + .version(group.getTargetVersion()) + .code(target.getCode()) + .display(target.getDisplay()) + .build()) + .source(source) + .build()); + } + } + } + return TranslationOutcome.builder() + .result(match.isEmpty() ? Boolean.FALSE : Boolean.TRUE) + .message(match.isEmpty() ? string("No matches found") : null) + .match(match) + .build(); + } + + /** + * Translate the given system, version and code using the provided concept map + * + * @param conceptMap + * the concept map + * @param system + * the system * @param version * the version * @param code * the code - * @param display - * the display * @return - * the outcome of validation + * the outcome of translation */ - public ValidationOutcome validateCode(CodeSystem codeSystem, String version, Code code, String display) { - return validateCode(codeSystem, version, code, display, ValidationParameters.EMPTY); + public TranslationOutcome translate(ConceptMap conceptMap, Uri system, String version, Code code) { + return translate(conceptMap, system, version, code, TranslationParameters.EMPTY); } /** - * Validate a coding using the provided code system and validation parameters + * Translate the given system, version and code using the provided concept map and translation parameters * - * @param codeSystem - * the code system - * @param coding - * the coding + * @param conceptMap + * the concept map + * @param system + * the system + * @param version + * the version + * @param code + * the code * @param parameters - * the validation parameters + * the translation parameters * @return - * the outcome of validation + * the outcome of translation */ - public ValidationOutcome validateCode(CodeSystem codeSystem, Coding coding, ValidationParameters parameters) { - if (!ValidationParameters.EMPTY.equals(parameters)) { - throw new UnsupportedOperationException("Validation parameters are not supported"); + public TranslationOutcome translate(ConceptMap conceptMap, Uri system, String version, Code code, TranslationParameters parameters) { + if (!TranslationParameters.EMPTY.equals(parameters)) { + throw new UnsupportedOperationException("Translation parameters are not supported"); } - LookupOutcome outcome = lookup(coding, LookupParameters.EMPTY); - return validateCode(codeSystem, coding, (outcome != null), outcome); + Coding coding = Coding.builder() + .system(system) + .version(version) + .code(code) + .build(); + return translate(conceptMap, coding, parameters); } /** - * Validate a coding using the provided code system + * Validate a codeable concept using the provided code system * - * @param codeSystem - * the codeSystem - * @param coding - * the coding + * @param codeableConcept + * the codeable concept * @return * the outcome of validation */ - public ValidationOutcome validateCode(CodeSystem codeSystem, Coding coding) { - return validateCode(codeSystem, coding, ValidationParameters.EMPTY); + public ValidationOutcome validateCode(CodeSystem codeSystem, CodeableConcept codeableConcept) { + return validateCode(codeSystem, codeableConcept, ValidationParameters.EMPTY); } /** @@ -527,64 +608,62 @@ public ValidationOutcome validateCode(CodeSystem codeSystem, CodeableConcept cod } /** - * Validate a codeable concept using the provided code system + * Validate a coding using the provided code system * - * @param codeableConcept - * the codeable concept + * @param codeSystem + * the codeSystem + * @param coding + * the coding * @return * the outcome of validation */ - public ValidationOutcome validateCode(CodeSystem codeSystem, CodeableConcept codeableConcept) { - return validateCode(codeSystem, codeableConcept, ValidationParameters.EMPTY); + public ValidationOutcome validateCode(CodeSystem codeSystem, Coding coding) { + return validateCode(codeSystem, coding, ValidationParameters.EMPTY); } /** - * Validate a code using the provided value set and validation parameters + * Validate a coding using the provided code system and validation parameters * - * @apiNote - * the implementation will expand the provided value set if needed - * @param valueSet - * the value set - * @param code - * the code + * @param codeSystem + * the code system + * @param coding + * the coding * @param parameters * the validation parameters * @return * the outcome of validation */ - public ValidationOutcome validateCode(ValueSet valueSet, Code code, ValidationParameters parameters) { + public ValidationOutcome validateCode(CodeSystem codeSystem, Coding coding, ValidationParameters parameters) { if (!ValidationParameters.EMPTY.equals(parameters)) { throw new UnsupportedOperationException("Validation parameters are not supported"); } - boolean result = ValueSetSupport.validateCode(valueSet, code); - return validateCode(null, Coding.builder().code(code).build(), result, null); + LookupOutcome outcome = lookup(coding, LookupParameters.EMPTY); + return validateCode(codeSystem, coding, (outcome != null), outcome); } /** - * Validate a code using the provided value set + * Validate a code and display using the provided code system and version * - * @apiNote - * the implementation will expand the provided value set if needed - * @param valueSet - * the value set + * @param code system + * the code system + * @param version + * the version * @param code * the code + * @param display + * the display * @return * the outcome of validation */ - public ValidationOutcome validateCode(ValueSet valueSet, Code code) { - return validateCode(valueSet, code, ValidationParameters.EMPTY); + public ValidationOutcome validateCode(CodeSystem codeSystem, String version, Code code, String display) { + return validateCode(codeSystem, version, code, display, ValidationParameters.EMPTY); } /** - * Validate a code and display using the provided value set and validation parameters + * Validate a code and display using the provided code system, version and validation parameters * - * @apiNote - * the implementation will expand the provided value set if needed - * @param valueSet - * the value set - * @param system - * the system + * @param codeSystem + * the code system * @param version * the version * @param code @@ -596,79 +675,70 @@ public ValidationOutcome validateCode(ValueSet valueSet, Code code) { * @return * the outcome of validation */ - public ValidationOutcome validateCode(ValueSet valueSet, Uri system, String version, Code code, String display, ValidationParameters parameters) { + public ValidationOutcome validateCode(CodeSystem codeSystem, String version, Code code, String display, ValidationParameters parameters) { if (!ValidationParameters.EMPTY.equals(parameters)) { throw new UnsupportedOperationException("Validation parameters are not supported"); } Coding coding = Coding.builder() - .system(system) .version(version) .code(code) + .display(display) .build(); - return validateCode(valueSet, coding, parameters); + return validateCode(codeSystem, coding, parameters); } /** - * Validate a code and display using the provided value set + * Validate a code using the provided value set * * @apiNote * the implementation will expand the provided value set if needed * @param valueSet * the value set - * @param system - * the system - * @param version - * the version * @param code * the code - * @param display - * the display * @return * the outcome of validation */ - public ValidationOutcome validateCode(ValueSet valueSet, Uri system, String version, Code code, String display) { - return validateCode(valueSet, system, version, code, display, ValidationParameters.EMPTY); + public ValidationOutcome validateCode(ValueSet valueSet, Code code) { + return validateCode(valueSet, code, ValidationParameters.EMPTY); } /** - * Validate a coding using the provided value set using the provided validation parameters + * Validate a code using the provided value set and validation parameters * * @apiNote * the implementation will expand the provided value set if needed * @param valueSet * the value set - * @param coding - * the coding + * @param code + * the code * @param parameters * the validation parameters * @return * the outcome of validation */ - public ValidationOutcome validateCode(ValueSet valueSet, Coding coding, ValidationParameters parameters) { + public ValidationOutcome validateCode(ValueSet valueSet, Code code, ValidationParameters parameters) { if (!ValidationParameters.EMPTY.equals(parameters)) { throw new UnsupportedOperationException("Validation parameters are not supported"); } - boolean result = ValueSetSupport.validateCode(valueSet, coding); - LookupOutcome outcome = result ? lookup(coding) : null; - return validateCode(coding, result, outcome); + boolean result = ValueSetSupport.validateCode(valueSet, code); + return validateCode(null, Coding.builder().code(code).build(), result, null); } /** - * Validate a coding using the provided value set using the provided validation parameters + * Validate a codeable concept using the provided value set * * @apiNote * the implementation will expand the provided value set if needed * @param valueSet * the value set - * @param coding - * the coding - * @param parameters - * the validation parameters + * @param codeable concept + * the codeable concept * @return * the outcome of validation */ - public ValidationOutcome validateCode(ValueSet valueSet, Coding coding) { - return validateCode(valueSet, coding, ValidationParameters.EMPTY); + public ValidationOutcome validateCode(ValueSet valueSet, CodeableConcept codeableConcept) { + return validateCode(valueSet, codeableConcept, ValidationParameters.EMPTY); } /** @@ -700,172 +770,115 @@ public ValidationOutcome validateCode(ValueSet valueSet, CodeableConcept codeabl } /** - * Validate a codeable concept using the provided value set + * Validate a coding using the provided value set using the provided validation parameters * * @apiNote * the implementation will expand the provided value set if needed * @param valueSet * the value set - * @param codeable concept - * the codeable concept + * @param coding + * the coding + * @param parameters + * the validation parameters * @return * the outcome of validation */ - public ValidationOutcome validateCode(ValueSet valueSet, CodeableConcept codeableConcept) { - return validateCode(valueSet, codeableConcept, ValidationParameters.EMPTY); + public ValidationOutcome validateCode(ValueSet valueSet, Coding coding) { + return validateCode(valueSet, coding, ValidationParameters.EMPTY); } /** - * Translate the given system, version and code using the provided concept map and translation parameters + * Validate a coding using the provided value set using the provided validation parameters * - * @param conceptMap - * the concept map - * @param system - * the system - * @param version - * the version - * @param code - * the code + * @apiNote + * the implementation will expand the provided value set if needed + * @param valueSet + * the value set + * @param coding + * the coding * @param parameters - * the translation parameters + * the validation parameters * @return - * the outcome of translation + * the outcome of validation */ - public TranslationOutcome translate(ConceptMap conceptMap, Uri system, String version, Code code, TranslationParameters parameters) { - if (!TranslationParameters.EMPTY.equals(parameters)) { - throw new UnsupportedOperationException("Translation parameters are not supported"); + public ValidationOutcome validateCode(ValueSet valueSet, Coding coding, ValidationParameters parameters) { + if (!ValidationParameters.EMPTY.equals(parameters)) { + throw new UnsupportedOperationException("Validation parameters are not supported"); } - Coding coding = Coding.builder() - .system(system) - .version(version) - .code(code) - .build(); - return translate(conceptMap, coding, parameters); + boolean result = ValueSetSupport.validateCode(valueSet, coding); + LookupOutcome outcome = result ? lookup(coding) : null; + return validateCode(coding, result, outcome); } /** - * Translate the given system, version and code using the provided concept map + * Validate a code and display using the provided value set * - * @param conceptMap - * the concept map + * @apiNote + * the implementation will expand the provided value set if needed + * @param valueSet + * the value set * @param system * the system * @param version * the version * @param code * the code + * @param display + * the display * @return - * the outcome of translation + * the outcome of validation */ - public TranslationOutcome translate(ConceptMap conceptMap, Uri system, String version, Code code) { - return translate(conceptMap, system, version, code, TranslationParameters.EMPTY); + public ValidationOutcome validateCode(ValueSet valueSet, Uri system, String version, Code code, String display) { + return validateCode(valueSet, system, version, code, display, ValidationParameters.EMPTY); } /** - * Translate the given coding using the provided concept map and translation parameters + * Validate a code and display using the provided value set and validation parameters * - * @param conceptMap - * the concept map - * @param coding - * the coding + * @apiNote + * the implementation will expand the provided value set if needed + * @param valueSet + * the value set + * @param system + * the system + * @param version + * the version + * @param code + * the code + * @param display + * the display * @param parameters - * the translation parameters + * the validation parameters * @return - * the outcome of translation + * the outcome of validation */ - public TranslationOutcome translate(ConceptMap conceptMap, Coding coding, TranslationParameters parameters) { - if (!TranslationParameters.EMPTY.equals(parameters)) { - throw new UnsupportedOperationException("Translation parameters are not supported"); - } - Uri source = getSource(conceptMap); - List match = new ArrayList<>(); - for (Group group : conceptMap.getGroup()) { - if (group.getSource() == null || !group.getSource().equals(coding.getSystem())) { - continue; - } - if (group.getSourceVersion() != null && coding.getVersion() != null && !group.getSourceVersion().equals(coding.getVersion())) { - continue; - } - for (Element element : group.getElement()) { - if (element.getCode() == null || !element.getCode().equals(coding.getCode())) { - // TODO: handle unmatched codes here - continue; - } - for (Target target : element.getTarget()) { - match.add(Match.builder() - .equivalence(target.getEquivalence()) - .concept(Coding.builder() - .system(group.getTarget()) - .version(group.getTargetVersion()) - .code(target.getCode()) - .display(target.getDisplay()) - .build()) - .source(source) - .build()); - } - } + public ValidationOutcome validateCode(ValueSet valueSet, Uri system, String version, Code code, String display, ValidationParameters parameters) { + if (!ValidationParameters.EMPTY.equals(parameters)) { + throw new UnsupportedOperationException("Validation parameters are not supported"); } - return TranslationOutcome.builder() - .result(match.isEmpty() ? Boolean.FALSE : Boolean.TRUE) - .message(match.isEmpty() ? string("No matches found") : null) - .match(match) + Coding coding = Coding.builder() + .system(system) + .version(version) + .code(code) .build(); + return validateCode(valueSet, coding, parameters); } - /** - * Translate the given coding using the provided concept map - * - * @param conceptMap - * the concept map - * @param coding - * the coding - * @return - * the outcome of translation - */ - public TranslationOutcome translate(ConceptMap conceptMap, Coding coding) { - return translate(conceptMap, coding, TranslationParameters.EMPTY); - } - - /** - * Translate the given codeable concept using the provided concept map and translation parameters - * - * @param conceptMap - * the concept map - * @param codeableConcept - * the codeable concept - * @param parameters - * the translation parameters - * @return - * the outcome of translation - */ - public TranslationOutcome translate(ConceptMap conceptMap, CodeableConcept codeableConcept, TranslationParameters parameters) { - if (!TranslationParameters.EMPTY.equals(parameters)) { - throw new UnsupportedOperationException("Translation parameters are not supported"); - } - for (Coding coding : codeableConcept.getCoding()) { - TranslationOutcome outcome = translate(conceptMap, coding, parameters); - if (Boolean.TRUE.equals(outcome.getResult())) { - return outcome; + private FHIRTermServiceProvider findProvider(CodeSystem codeSystem) { + for (FHIRTermServiceProvider provider : providers) { + if (provider.isSupported(codeSystem)) { + return provider; } } - return TranslationOutcome.builder() - .result(Boolean.FALSE) - .message(string("No matches found")) - .build(); + return NULL_TERM_SERVICE_PROVIDER; } - /** - * Translate the given coding using the provided concept map - * - * @param conceptMap - * the concept map - * @param codeable concept - * the codeable concept - * @return - * the outcome of translation - */ - public TranslationOutcome translate(ConceptMap conceptMap, CodeableConcept codeableConcept) { - return translate(conceptMap, codeableConcept, TranslationParameters.EMPTY); + private Uri getSource(ConceptMap conceptMap) { + StringBuilder sb = new StringBuilder(conceptMap.getUrl().getValue()); + if (conceptMap.getVersion() != null) { + sb.append("|").append(conceptMap.getVersion().getValue()); + } + return Uri.of(sb.toString()); } private List loadProviders() { @@ -878,19 +891,6 @@ private List loadProviders() { return providers; } - private FHIRTermServiceProvider findProvider(CodeSystem codeSystem) { - for (FHIRTermServiceProvider provider : providers) { - if (provider.isSupported(codeSystem)) { - return provider; - } - } - return NULL_TERM_SERVICE_PROVIDER; - } - - private ValidationOutcome validateCode(Coding coding, boolean result, LookupOutcome outcome) { - return validateCode(null, coding, result, outcome); - } - private ValidationOutcome validateCode(CodeSystem codeSystem, Coding coding, boolean result, LookupOutcome outcome) { java.lang.String message = null; if (!result && coding != null && coding.getCode() != null) { @@ -919,11 +919,11 @@ private ValidationOutcome validateCode(CodeSystem codeSystem, Coding coding, boo .build(); } - private Uri getSource(ConceptMap conceptMap) { - StringBuilder sb = new StringBuilder(conceptMap.getUrl().getValue()); - if (conceptMap.getVersion() != null) { - sb.append("|").append(conceptMap.getVersion().getValue()); - } - return Uri.of(sb.toString()); + private ValidationOutcome validateCode(Coding coding, boolean result, LookupOutcome outcome) { + return validateCode(null, coding, result, outcome); + } + + public static FHIRTermService getInstance() { + return INSTANCE; } } diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java b/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java index 24dc8467a01..a1622b487e8 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java @@ -22,13 +22,9 @@ */ public class DefaultTermServiceProvider implements FHIRTermServiceProvider { @Override - public boolean isSupported(CodeSystem codeSystem) { - return CodeSystemContentMode.COMPLETE.equals(codeSystem.getContent()); - } - - @Override - public boolean hasConcept(CodeSystem codeSystem, Code code) { - return getConcept(codeSystem, code) != null; + public Set closure(CodeSystem codeSystem, Code code) { + Concept concept = CodeSystemSupport.findConcept(codeSystem, code); + return CodeSystemSupport.getConcepts(concept); } @Override @@ -47,14 +43,18 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { } @Override - public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { - Concept concept = CodeSystemSupport.findConcept(codeSystem, codeA); - return (CodeSystemSupport.findConcept(codeSystem, concept, codeB) != null); + public boolean hasConcept(CodeSystem codeSystem, Code code) { + return getConcept(codeSystem, code) != null; } @Override - public Set closure(CodeSystem codeSystem, Code code) { - Concept concept = CodeSystemSupport.findConcept(codeSystem, code); - return CodeSystemSupport.getConcepts(concept); + public boolean isSupported(CodeSystem codeSystem) { + return CodeSystemContentMode.COMPLETE.equals(codeSystem.getContent()); + } + + @Override + public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { + Concept concept = CodeSystemSupport.findConcept(codeSystem, codeA); + return (CodeSystemSupport.findConcept(codeSystem, concept, codeB) != null); } } diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/spi/FHIRTermServiceProvider.java b/fhir-term/src/main/java/com/ibm/fhir/term/spi/FHIRTermServiceProvider.java index db01f568af8..767eaa045f3 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/spi/FHIRTermServiceProvider.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/spi/FHIRTermServiceProvider.java @@ -16,26 +16,17 @@ public interface FHIRTermServiceProvider { /** - * Indicates whether the given code system is supported. - * - * @param codeSystem - * the code system - * @return - * true if the given code system is supported, false otherwise - */ - boolean isSupported(CodeSystem codeSystem); - - /** - * Indicates whether the given code system contains a concept with the specified code. + * Get a set containing {@link CodeSystem.Concept} instances where all structural + * hierarchies have been flattened. * * @param codeSystem * the code system * @param code - * the code + * the root of the hierarchy containing the Concept instances to be flattened * @return - * true if the given code system contains a concept with the specified code, false otherwise + * flattened set of Concept instances for the given tree */ - boolean hasConcept(CodeSystem codeSystem, Code code); + Set closure(CodeSystem codeSystem, Code code); /** * Get the concept in the provided code system with the specified code. @@ -74,29 +65,38 @@ public interface FHIRTermServiceProvider { Set getConcepts(CodeSystem codeSystem, List filters); /** - * Find the concept in tree rooted by the provided concept that matches the specified code. + * Indicates whether the given code system contains a concept with the specified code. * * @param codeSystem * the code system - * @param codeA - * the root of the hierarchy to search - * @param codeB - * the code to match + * @param code + * the code * @return - * the code system concept that matches the specified code, or null if not such concept exists + * true if the given code system contains a concept with the specified code, false otherwise */ - boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB); + boolean hasConcept(CodeSystem codeSystem, Code code); /** - * Get a set containing {@link CodeSystem.Concept} instances where all structural - * hierarchies have been flattened. + * Indicates whether the given code system is supported. * * @param codeSystem * the code system - * @param code - * the root of the hierarchy containing the Concept instances to be flattened * @return - * flattened set of Concept instances for the given tree + * true if the given code system is supported, false otherwise */ - Set closure(CodeSystem codeSystem, Code code); + boolean isSupported(CodeSystem codeSystem); + + /** + * Find the concept in tree rooted by the provided concept that matches the specified code. + * + * @param codeSystem + * the code system + * @param codeA + * the root of the hierarchy to search + * @param codeB + * the code to match + * @return + * the code system concept that matches the specified code, or null if not such concept exists + */ + boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB); } \ No newline at end of file From 10182218b251ca1008f27d28b8b8038837bffd91 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Wed, 3 Mar 2021 14:08:57 -0500 Subject: [PATCH 08/50] Issue #1980 - updates Signed-off-by: John T.E. Timm --- .../graph/provider/GraphTermServiceProvider.java | 4 ++-- .../ibm/fhir/term/graph/util/FHIRTermGraphUtil.java | 2 +- .../src/main/resources/conf/local-graph.properties | 12 ------------ .../graph/test/CodeSystemTermGraphLoaderTest.java | 2 +- .../ibm/fhir/term/graph/test/FHIRTermGraphTest.java | 2 +- ...rties => janusgraph-berkeleyje-lucene.properties} | 1 - .../janusgraph-cassandra-elasticsearch.properties | 8 ++++++++ 7 files changed, 13 insertions(+), 18 deletions(-) delete mode 100644 fhir-term-graph/src/main/resources/conf/local-graph.properties rename fhir-term-graph/src/test/resources/conf/{local-graph.properties => janusgraph-berkeleyje-lucene.properties} (83%) create mode 100644 fhir-term-graph/src/test/resources/conf/janusgraph-cassandra-elasticsearch.properties 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 25ed71e1aa8..b54d5548636 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 @@ -175,7 +175,7 @@ private Designation createDesignation(Map elementMap, String des private Property createProperty(Map elementMap) { return Property.builder() .code(Code.of((String) elementMap.get("code"))) - .value(getElement(elementMap)) + .value(getValue(elementMap)) .build(); } @@ -224,7 +224,7 @@ private String getDesignationUseSystem(CodeSystem codeSystem) { return null; } - private Element getElement(Map elementMap) { + private Element getValue(Map elementMap) { for (Object value : elementMap.values()) { if (value instanceof Element) { return (Element) value; diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java index b4a1373ed2e..dd649799c47 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java @@ -41,7 +41,7 @@ public static void setRootLoggerLevel(Level level) { } public static String toLabel(String typeName) { - List tokens = Arrays.asList(typeName.split(" |_|-")); + List tokens = Arrays.asList(typeName.split(" - | |_|-")); String label = tokens.stream() .map(token -> token.substring(0, 1).toUpperCase() + token.substring(1)) .collect(Collectors.joining("")); diff --git a/fhir-term-graph/src/main/resources/conf/local-graph.properties b/fhir-term-graph/src/main/resources/conf/local-graph.properties deleted file mode 100644 index 37c6c843f4e..00000000000 --- a/fhir-term-graph/src/main/resources/conf/local-graph.properties +++ /dev/null @@ -1,12 +0,0 @@ -#storage.backend=berkeleyje -#storage.directory=/tmp/data/graph -#index.search.backend=lucene -#index.search.directory=/tmp/data/searchindex -cache.tx-cache-size=100000 -cache.tx-dirty-size=10000 -ids.block-size=500000 -index.search.backend=elasticsearch -index.search.hostname=127.0.0.1:9200 -storage.backend=cql -storage.batch-loading=true -storage.hostname=127.0.0.1 \ No newline at end of file diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java index 0f7dd1883ca..b43a9c9b237 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java @@ -23,7 +23,7 @@ public class CodeSystemTermGraphLoaderTest { public static void main(String[] args) throws Exception { try (InputStream in = CodeSystemTermGraphLoaderTest.class.getClassLoader().getResourceAsStream("JSON/CodeSystem-cs5.json")) { CodeSystem codeSystem = FHIRParser.parser(Format.JSON).parse(in); - FHIRTermGraph termGraph = FHIRTermGraphFactory.open("conf/local-graph.properties"); + FHIRTermGraph termGraph = FHIRTermGraphFactory.open("conf/janusgraph-berkeleyje-lucene.properties"); CodeSystemTermGraphLoader loader = new CodeSystemTermGraphLoader(termGraph, codeSystem); loader.load(); diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java index 5bb79fff9a2..eda4d3b24dd 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java @@ -19,7 +19,7 @@ public class FHIRTermGraphTest { public static void main(String[] args) throws Exception { - FHIRTermGraph graph = FHIRTermGraphFactory.open("conf/local-graph.properties"); + FHIRTermGraph graph = FHIRTermGraphFactory.open("conf/janusgraph-berkeleyje-lucene.properties"); GraphTraversalSource g = graph.traversal(); diff --git a/fhir-term-graph/src/test/resources/conf/local-graph.properties b/fhir-term-graph/src/test/resources/conf/janusgraph-berkeleyje-lucene.properties similarity index 83% rename from fhir-term-graph/src/test/resources/conf/local-graph.properties rename to fhir-term-graph/src/test/resources/conf/janusgraph-berkeleyje-lucene.properties index 6e1e4e75f8e..d2bb6e67dee 100644 --- a/fhir-term-graph/src/test/resources/conf/local-graph.properties +++ b/fhir-term-graph/src/test/resources/conf/janusgraph-berkeleyje-lucene.properties @@ -1,5 +1,4 @@ storage.backend=berkeleyje storage.directory=target/data/graph -#storage.batch-loading=true index.search.backend=lucene index.search.directory=target/data/searchindex \ No newline at end of file diff --git a/fhir-term-graph/src/test/resources/conf/janusgraph-cassandra-elasticsearch.properties b/fhir-term-graph/src/test/resources/conf/janusgraph-cassandra-elasticsearch.properties new file mode 100644 index 00000000000..5f252c4bd92 --- /dev/null +++ b/fhir-term-graph/src/test/resources/conf/janusgraph-cassandra-elasticsearch.properties @@ -0,0 +1,8 @@ +storage.backend=cql +storage.batch-loading=true +storage.hostname=127.0.0.1 +index.search.backend=elasticsearch +index.search.hostname=127.0.0.1:9200 +cache.tx-cache-size=100000 +cache.tx-dirty-size=10000 +ids.block-size=500000 \ No newline at end of file From 49f149165c370ea92b1ab01aaed9b249654881c9 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Fri, 5 Mar 2021 12:00:21 -0500 Subject: [PATCH 09/50] Issue #1980 - updates Signed-off-by: John T.E. Timm --- .../ibm/fhir/term/graph/FHIRTermGraph.java | 4 + .../{ => factory}/FHIRTermGraphFactory.java | 3 +- .../term/graph/impl/FHIRTermGraphImpl.java | 42 +-- .../loader/CodeSystemTermGraphLoader.java | 121 ------- .../graph/loader/FHIRTermGraphLoader.java | 52 +++ .../graph/loader/SnomedTermGraphLoader.java | 295 ---------------- .../factory/FHIRTermGraphLoaderFactory.java | 30 ++ .../loader/impl/AbstractTermGraphLoader.java | 57 ++++ .../impl/CodeSystemTermGraphLoader.java | 205 +++++++++++ .../loader/impl/SnomedTermGraphLoader.java | 319 ++++++++++++++++++ .../{ => impl}/UMLSTermGraphLoader.java | 187 +++++----- .../loader/main/FHIRTermGraphLoaderMain.java | 73 ++++ .../loader/util/FHIRTermGraphLoaderUtil.java | 52 +++ .../term/graph/loader/util/LabelFilter.java | 37 ++ .../provider/GraphTermServiceProvider.java | 53 +-- .../graph/serialize/BooleanSerializer.java | 25 -- .../term/graph/serialize/CodeSerializer.java | 32 -- .../graph/serialize/DateTimeSerializer.java | 31 -- .../graph/serialize/DecimalSerializer.java | 25 -- .../graph/serialize/IntegerSerializer.java | 25 -- .../graph/serialize/StringSerializer.java | 33 -- .../term/graph/util/FHIRTermGraphUtil.java | 70 +++- .../test/CodeSystemTermGraphLoaderTest.java | 20 +- .../term/graph/test/FHIRTermGraphTest.java | 22 +- 24 files changed, 1021 insertions(+), 792 deletions(-) rename fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/{ => factory}/FHIRTermGraphFactory.java (88%) delete mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/CodeSystemTermGraphLoader.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java delete mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/SnomedTermGraphLoader.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/factory/FHIRTermGraphLoaderFactory.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/AbstractTermGraphLoader.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/SnomedTermGraphLoader.java rename fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/{ => impl}/UMLSTermGraphLoader.java (77%) create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/main/FHIRTermGraphLoaderMain.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/util/FHIRTermGraphLoaderUtil.java create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/util/LabelFilter.java delete mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/BooleanSerializer.java delete mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/CodeSerializer.java delete mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DateTimeSerializer.java delete mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DecimalSerializer.java delete mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/IntegerSerializer.java delete mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/StringSerializer.java diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java index ec590f431e9..fbb111535b9 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java @@ -14,7 +14,11 @@ import org.janusgraph.core.JanusGraphIndexQuery.Result; import org.janusgraph.core.JanusGraphVertex; +/* + * A graph that represents FHIR CodeSystem content and is backed by a graph database (Janusgraph) + */ public interface FHIRTermGraph { + public static final String ISA = "isa"; Configuration configuration(); JanusGraph getJanusGraph(); GraphTraversalSource traversal(); diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraphFactory.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/factory/FHIRTermGraphFactory.java similarity index 88% rename from fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraphFactory.java rename to fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/factory/FHIRTermGraphFactory.java index a2c37c79dbc..915739a98c2 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraphFactory.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/factory/FHIRTermGraphFactory.java @@ -4,11 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -package com.ibm.fhir.term.graph; +package com.ibm.fhir.term.graph.factory; import org.apache.commons.configuration.Configuration; import org.apache.commons.configuration.PropertiesConfiguration; +import com.ibm.fhir.term.graph.FHIRTermGraph; import com.ibm.fhir.term.graph.impl.FHIRTermGraphImpl; public final class FHIRTermGraphFactory { diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java index 9270dfbd08b..77bf2d28dde 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java @@ -24,9 +24,6 @@ import org.janusgraph.core.RelationType; import org.janusgraph.core.schema.JanusGraphManagement; -import com.ibm.fhir.model.type.Code; -import com.ibm.fhir.model.type.DateTime; -import com.ibm.fhir.model.type.Decimal; import com.ibm.fhir.term.graph.FHIRTermGraph; public class FHIRTermGraphImpl implements FHIRTermGraph { @@ -53,8 +50,6 @@ private JanusGraph open(Configuration configuration) { boolean readOnly = configuration.getBoolean(STORAGE_READ_ONLY, false); configuration.setProperty(STORAGE_READ_ONLY, false); - addCustomAttributeSerializers(configuration); - setRootLoggerLevel(ch.qos.logback.classic.Level.INFO); JanusGraph graph = JanusGraphFactory.open(configuration); @@ -71,21 +66,6 @@ private JanusGraph open(Configuration configuration) { return graph; } - private void addCustomAttributeSerializers(Configuration configuration) { - configuration.setProperty("attributes.custom.attribute1.attribute-class", "com.ibm.fhir.model.type.Boolean"); - configuration.setProperty("attributes.custom.attribute1.serializer-class", "com.ibm.fhir.term.graph.serialize.BooleanSerializer"); - configuration.setProperty("attributes.custom.attribute2.attribute-class", "com.ibm.fhir.model.type.Code"); - configuration.setProperty("attributes.custom.attribute2.serializer-class", "com.ibm.fhir.term.graph.serialize.CodeSerializer"); - configuration.setProperty("attributes.custom.attribute3.attribute-class", "com.ibm.fhir.model.type.DateTime"); - configuration.setProperty("attributes.custom.attribute3.serializer-class", "com.ibm.fhir.term.graph.serialize.DateTimeSerializer"); - configuration.setProperty("attributes.custom.attribute4.attribute-class", "com.ibm.fhir.model.type.Decimal"); - configuration.setProperty("attributes.custom.attribute4.serializer-class", "com.ibm.fhir.term.graph.serialize.DecimalSerializer"); - configuration.setProperty("attributes.custom.attribute5.attribute-class", "com.ibm.fhir.model.type.Integer"); - configuration.setProperty("attributes.custom.attribute5.serializer-class", "com.ibm.fhir.term.graph.serialize.IntegerSerializer"); - configuration.setProperty("attributes.custom.attribute6.attribute-class", "com.ibm.fhir.model.type.String"); - configuration.setProperty("attributes.custom.attribute6.serializer-class", "com.ibm.fhir.term.graph.serialize.StringSerializer"); - } - private void createSchema(JanusGraph graph) { log.info("Creating schema..."); @@ -96,26 +76,23 @@ private void createSchema(JanusGraph graph) { PropertyKey code = management.makePropertyKey("code").dataType(String.class).make(); PropertyKey codeLowerCase = management.makePropertyKey("codeLowerCase").dataType(String.class).make(); PropertyKey display = management.makePropertyKey("display").dataType(String.class).make(); - PropertyKey displayLowerCase = management.makePropertyKey("displayLowerCase").dataType(String.class).make(); PropertyKey url = management.makePropertyKey("url").dataType(String.class).make(); PropertyKey value = management.makePropertyKey("value").dataType(String.class).make(); - PropertyKey valueBoolean = management.makePropertyKey("valueBoolean").dataType(com.ibm.fhir.model.type.Boolean.class).make(); - PropertyKey valueCode = management.makePropertyKey("valueCode").dataType(Code.class).make(); - PropertyKey valueDateTime = management.makePropertyKey("valueDateTime").dataType(DateTime.class).make(); - PropertyKey valueDecimal = management.makePropertyKey("valueDecimal").dataType(Decimal.class).make(); - PropertyKey valueInteger = management.makePropertyKey("valueInteger").dataType(com.ibm.fhir.model.type.Integer.class).make(); - PropertyKey valueString = management.makePropertyKey("valueString").dataType(com.ibm.fhir.model.type.String.class).make(); - + PropertyKey valueBoolean = management.makePropertyKey("valueBoolean").dataType(Boolean.class).make(); + PropertyKey valueCode = management.makePropertyKey("valueCode").dataType(String.class).make(); + PropertyKey valueDateTime = management.makePropertyKey("valueDateTime").dataType(String.class).make(); + PropertyKey valueDateTimeLong = management.makePropertyKey("valueDateTimeLong").dataType(Long.class).make(); + PropertyKey valueDecimal = management.makePropertyKey("valueDecimal").dataType(Double.class).make(); + PropertyKey valueInteger = management.makePropertyKey("valueInteger").dataType(Integer.class).make(); + PropertyKey valueString = management.makePropertyKey("valueString").dataType(String.class).make(); // property keys (not indexed) management.makePropertyKey("count").dataType(Integer.class).make(); - management.makePropertyKey("group").dataType(Integer.class).make(); + management.makePropertyKey("group").dataType(String.class).make(); management.makePropertyKey("language").dataType(String.class).make(); management.makePropertyKey("system").dataType(String.class).make(); management.makePropertyKey("use").dataType(String.class).make(); - management.makePropertyKey("caseSensitive").dataType(Boolean.class).make(); - management.makePropertyKey("designationUseSystem").dataType(String.class).make(); // vertex labels management.makeVertexLabel("CodeSystem").make(); @@ -124,6 +101,7 @@ private void createSchema(JanusGraph graph) { management.makeVertexLabel("Property_").make(); // edge labels + management.makeEdgeLabel(FHIRTermGraph.ISA).make(); management.makeEdgeLabel("concept").make(); management.makeEdgeLabel("designation").make(); management.makeEdgeLabel("property_").make(); @@ -132,7 +110,6 @@ private void createSchema(JanusGraph graph) { management.buildIndex("byCode", Vertex.class).addKey(code).buildCompositeIndex(); management.buildIndex("byCodeLowerCase", Vertex.class).addKey(codeLowerCase).buildCompositeIndex(); management.buildIndex("byDisplay", Vertex.class).addKey(display).buildCompositeIndex(); - management.buildIndex("byDisplayLowerCase", Vertex.class).addKey(displayLowerCase).buildCompositeIndex(); management.buildIndex("byUrl", Vertex.class).addKey(url).buildCompositeIndex(); management.buildIndex("byUrlAndVersion", Vertex.class).addKey(url).addKey(version).buildCompositeIndex(); management.buildIndex("byValue", Vertex.class).addKey(value).buildCompositeIndex(); @@ -140,6 +117,7 @@ private void createSchema(JanusGraph graph) { management.buildIndex("byValueBoolean", Vertex.class).addKey(valueBoolean).buildCompositeIndex(); management.buildIndex("byValueCode", Vertex.class).addKey(valueCode).buildCompositeIndex(); management.buildIndex("byValueDateTime", Vertex.class).addKey(valueDateTime).buildCompositeIndex(); + management.buildIndex("byValueDateTimeLong", Vertex.class).addKey(valueDateTimeLong).buildCompositeIndex(); management.buildIndex("byValueDecimal", Vertex.class).addKey(valueDecimal).buildCompositeIndex(); management.buildIndex("byValueInteger", Vertex.class).addKey(valueInteger).buildCompositeIndex(); management.buildIndex("byValueString", Vertex.class).addKey(valueString).buildCompositeIndex(); diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/CodeSystemTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/CodeSystemTermGraphLoader.java deleted file mode 100644 index d5f46eac07e..00000000000 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/CodeSystemTermGraphLoader.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2021 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.ibm.fhir.term.graph.loader; - -import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; -import static com.ibm.fhir.term.util.CodeSystemSupport.getConcepts; -import static com.ibm.fhir.term.util.CodeSystemSupport.isCaseSensitive; - -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; -import org.apache.tinkerpop.gremlin.structure.Vertex; - -import com.ibm.fhir.model.resource.CodeSystem; -import com.ibm.fhir.model.resource.CodeSystem.Concept; -import com.ibm.fhir.model.resource.CodeSystem.Concept.Designation; -import com.ibm.fhir.model.resource.CodeSystem.Concept.Property; -import com.ibm.fhir.model.type.Element; -import com.ibm.fhir.term.graph.FHIRTermGraph; - -public class CodeSystemTermGraphLoader { - private final FHIRTermGraph termGraph; - private final CodeSystem codeSystem; - - public CodeSystemTermGraphLoader(FHIRTermGraph termGraph, CodeSystem codeSystem) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); - this.termGraph = Objects.requireNonNull(termGraph, "termGraph"); - this.codeSystem = Objects.requireNonNull(codeSystem, "codeSystem"); - } - - public void load() { - GraphTraversalSource g = termGraph.traversal(); - - Map conceptVertexMap = new HashMap<>(); - - String url = codeSystem.getUrl().getValue(); - - Vertex codeSystemVertex = g.addV("CodeSystem") - .property("url", url) - .property("designationUseSystem", url) - .property("caseSensitive", isCaseSensitive(codeSystem)) - .next(); - - if (codeSystem.getVersion() != null) { - g.V(codeSystemVertex) - .property("version", codeSystem.getVersion().getValue()) - .next(); - } - - Set concepts = getConcepts(codeSystem); - - for (Concept concept : concepts) { - String code = concept.getCode().getValue(); - Vertex conceptVertex = g.addV("Concept") - .property("code", code) - .property("codeLowerCase", normalize(code)) - .next(); - - if (concept.getDisplay() != null) { - String display = concept.getDisplay().getValue(); - g.V(conceptVertex) - .property("display", display) - .property("displayLowerCase", normalize(display)) - .next(); - } - - for (Designation designation : concept.getDesignation()) { - Vertex designationVertex = g.addV("Designation") - .property("value", designation.getValue().getValue()) - .next(); - - if (designation.getUse() != null) { - g.V(designationVertex) - .property("use", designation.getUse().getCode().getValue()) - .next(); - } - - if (designation.getLanguage() != null) { - g.V(designationVertex) - .property("language", designation.getLanguage().getValue()) - .next(); - } - - g.V(conceptVertex).addE("designation").to(designationVertex).next(); - } - - for (Property property : concept.getProperty()) { - Element value = property.getValue(); - Vertex propertyVertex = g.addV("Property_") - .property("code", property.getCode().getValue()) - .property("value" + value.getClass().getSimpleName(), value) - .next(); - - g.V(conceptVertex).addE("property_").to(propertyVertex).next(); - } - - g.V(codeSystemVertex).addE("concept").to(conceptVertex).next(); - - conceptVertexMap.put(concept, conceptVertex); - } - - for (Concept concept : concepts) { - Vertex v = conceptVertexMap.get(concept); - for (Concept child : concept.getConcept()) { - Vertex w = conceptVertexMap.get(child); - g.V(w).addE("isA").to(v).next(); - } - } - - g.tx().commit(); - - termGraph.close(); - } -} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java new file mode 100644 index 00000000000..d5da686b99b --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java @@ -0,0 +1,52 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.loader; + +import java.util.Map; + +import org.apache.commons.cli.Options; + +public interface FHIRTermGraphLoader { + enum Type { + CODESYSTEM { + @Override + public Options options() { + return new Options() + .addRequiredOption("config", null, true, "Configuration properties file") + .addOption("url", null, true, "CodeSystem url") + .addOption("file", null, true, "CodeSystem file"); + } + }, + SNOMED { + @Override + public Options options() { + return new Options() + .addRequiredOption("config", null, true, "Configuration properties file") + .addRequiredOption("base", null, true, "SNOMED-CT base directory") + .addRequiredOption("concept", null, true, "SNOMED-CT concept file") + .addRequiredOption("relation", null, true, "SNOMED-CT relationship file") + .addRequiredOption("desc", null, true, "SNOMED-CT description file") + .addRequiredOption("lang", null, true, "SNOMED-CT language refset file") + .addOption("labels", null, true, "labels"); + } + }, + UMLS { + @Override + public Options options() { + return new Options() + .addRequiredOption("config", null, true, "Configuration properties file") + .addRequiredOption("base", null, true, "UMLS base directory") + .addOption("labels", null, true, "labels"); + } + }; + + public abstract Options options(); + } + void load(); + void close(); + Map options(); +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/SnomedTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/SnomedTermGraphLoader.java deleted file mode 100644 index 75c2bd1a568..00000000000 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/SnomedTermGraphLoader.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2020, 2021 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.ibm.fhir.term.graph.loader; - -import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; -import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toLabel; - -import java.io.BufferedReader; -import java.io.FileReader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import java.util.logging.Logger; - -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.CommandLineParser; -import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.HelpFormatter; -import org.apache.commons.cli.MissingOptionException; -import org.apache.commons.cli.Options; -import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; -import org.apache.tinkerpop.gremlin.structure.Edge; -import org.apache.tinkerpop.gremlin.structure.Vertex; -import org.janusgraph.core.JanusGraph; -import org.janusgraph.core.schema.JanusGraphManagement; - -import com.ibm.fhir.term.graph.FHIRTermGraph; -import com.ibm.fhir.term.graph.FHIRTermGraphFactory; - -public class SnomedTermGraphLoader { - private static final Logger log = Logger.getLogger(SnomedTermGraphLoader.class.getName()); - - private static final String PREFERRED = "900000000000548007"; - private static final String FULLY_SPECIFIED_NAME = "900000000000003001"; - - public static void main(String[] args) throws Exception { - Options options = null; - FHIRTermGraph graph = null; - try { - long start = System.currentTimeMillis(); - - options = new Options() - .addRequiredOption("file", null, true, "Configuration properties file") - .addRequiredOption("base", null, true, "SNOMED-CT base directory") - .addRequiredOption("concept", null, true, "SNOMED-CT concept file") - .addRequiredOption("relation", null, true, "SNOMED-CT relationship file") - .addRequiredOption("desc", null, true, "SNOMED-CT description file") - .addRequiredOption("lang", null, true, "SNOMED-CT language refset file"); - - CommandLineParser parser = new DefaultParser(); - CommandLine commandLine = parser.parse(options, args); - - String propFileName = commandLine.getOptionValue("file"); - - graph = FHIRTermGraphFactory.open(propFileName); - final JanusGraph janusGraph = graph.getJanusGraph(); - - GraphTraversalSource g = graph.traversal(); - - String baseDir = commandLine.getOptionValue("base"); - - AtomicInteger counter = new AtomicInteger(0); - Map vertexMap = new HashMap<>(250000); - - Vertex codeSystemVertex = g.addV("CodeSystem") - .property("url", "http://snomed.info/sct") - .property("caseSensitive", false) - .property("designationUseSystem", "http://snomed.info.sct") - .next(); - g.tx().commit(); - - // concept file - log.info("Processing concepts file..."); - String conceptFile = baseDir + "/" + commandLine.getOptionValue("concept"); - try (BufferedReader reader = new BufferedReader(new FileReader(conceptFile))) { - reader.lines().skip(1).forEach(new SnomedReleaseFileConsumer() { - @Override - public void processLine(String line) { - String[] tokens = line.split("\\t"); - String id = tokens[0]; - String active = tokens[2]; - - if ("1".equals(active)) { - if (!vertexMap.containsKey(id)) { - Vertex v = g.addV("Concept") - .property("code", id) - .property("codeLowerCase", normalize(id)) - .next(); - vertexMap.put(id, v); - g.V(codeSystemVertex).addE("concept").to(v).next(); - } - } - - if ((counter.get() % 10000) == 0) { - log.info("counter: " + counter.get()); - g.tx().commit(); - } - - counter.getAndIncrement(); - } - }); - - // commit any uncommitted work - g.tx().commit(); - } - - int count = counter.get(); - g.V(codeSystemVertex).property("count", count).next(); - g.tx().commit(); - - // language refset file - log.info("Processing language refset file..."); - Set preferred = new HashSet<>(500000); - String languageRefsetFile = baseDir + "/../Refset/Language/" + commandLine.getOptionValue("lang"); - try (BufferedReader reader = new BufferedReader(new FileReader(languageRefsetFile))) { - reader.lines().skip(1).forEach(new SnomedReleaseFileConsumer() { - @Override - public void processLine(String line) { - String[] tokens = line.split("\\t"); - String active = tokens[2]; - String referencedComponentId = tokens[5]; - String acceptabilityId = tokens[6]; - - if ("1".equals(active) && PREFERRED.equals(acceptabilityId)) { - preferred.add(referencedComponentId); - } - } - }); - } - - counter.set(0); - - // description file - log.info("Processing description file..."); - String descriptionFile = baseDir + "/" + commandLine.getOptionValue("desc"); - try (BufferedReader reader = new BufferedReader(new FileReader(descriptionFile))) { - reader.lines().skip(1).forEach(new SnomedReleaseFileConsumer() { - @Override - public void processLine(String line) { - String[] tokens = line.split("\\t"); - String id = tokens[0]; - String active = tokens[2]; - String conceptId = tokens[4]; - String typeId = tokens[6]; - String term = tokens[7]; - - Vertex v = vertexMap.get(conceptId); - - if ("1".equals(active) && v != null) { - if (preferred.contains(id) && !FULLY_SPECIFIED_NAME.equals(typeId)) { - // preferred term - g.V(v) - .property("display", term) - .property("displayLowerCase", normalize(term)) - .next(); - } - - Vertex w = g.addV("Designation") - .property("language", "en") - .property("use", typeId) - .property("value", term) - .next(); - - g.V(v).addE("designation").to(w).next(); - } - - if ((counter.get() % 10000) == 0) { - log.info("counter: " + counter.get()); - g.tx().commit(); - } - - counter.getAndIncrement(); - } - }); - - // commit any uncommitted work - g.tx().commit(); - } - - counter.set(0); - - // relationship file - log.info("Processing relationship file..."); - String relationshipFile = baseDir + "/" + commandLine.getOptionValue("relation"); - try (BufferedReader reader = new BufferedReader(new FileReader(relationshipFile))) { - reader.lines().skip(1).forEach(new SnomedReleaseFileConsumer() { - @Override - public void processLine(String line) { - String[] tokens = line.split("\\t"); - String active = tokens[2]; - String sourceId = tokens[4]; - String destinationId = tokens[5]; - String relationshipGroup = tokens[6]; - String typeId = tokens[7]; - - if ("1".equals(active)) { - Vertex u = vertexMap.get(sourceId); - Vertex v = vertexMap.get(destinationId); - Vertex w = vertexMap.get(typeId); - - if (u != null && v != null && w != null) { - String display = (String) g.V(w).values("display").next(); - String label = toLabel(display); - - if (janusGraph.getEdgeLabel(label) == null) { - log.info("Adding label: " + label); - JanusGraphManagement management = janusGraph.openManagement(); - management.makeEdgeLabel(label).make(); - management.commit(); - } - - Edge e = g.V(u).addE(label).to(v).next(); - - if (!"0".equals(relationshipGroup)) { - g.E(e).property("group", Integer.parseInt(relationshipGroup)).next(); - } - } - } - - if ((counter.get() % 10000) == 0) { - log.info("counter: " + counter.get()); - g.tx().commit(); - } - - counter.getAndIncrement(); - } - }); - - // commit any uncommitted work - g.tx().commit(); - } - - long end = System.currentTimeMillis(); - - log.info("Loading time (milliseconds): " + (end - start)); - - graph.close(); - graph = null; - } catch (MissingOptionException e) { - System.out.println("MissingOptionException: " + e.getMessage()); - HelpFormatter formatter = new HelpFormatter(); - formatter.printHelp("SnomedTermGraphLoader", options); - } catch (Exception e) { - e.printStackTrace(); - System.out.println("An error occurred: " + e.getMessage()); - } finally { - if (graph != null) { - graph.close(); - } - } - } - - private static abstract class SnomedReleaseFileConsumer implements Consumer { - private final List lines = new ArrayList<>(); - private String previousId = null; - - @Override - public void accept(String line) { - if (collect(line)) { - lines.add(line); - } else { - processLines(Collections.unmodifiableList(lines)); - lines.clear(); - lines.add(line); - } - } - - private boolean collect(String line) { - String[] fields = line.split("\\t"); - String id = fields[0]; - if (!id.equals(previousId)) { - previousId = id; - return lines.isEmpty(); - } - return true; - } - - private void processLines(List lines) { - processLine(lines.get(lines.size() - 1)); - } - - // template method - public abstract void processLine(String line); - } -} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/factory/FHIRTermGraphLoaderFactory.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/factory/FHIRTermGraphLoaderFactory.java new file mode 100644 index 00000000000..dcbd3039de0 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/factory/FHIRTermGraphLoaderFactory.java @@ -0,0 +1,30 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.loader.factory; + +import java.util.Map; + +import com.ibm.fhir.term.graph.loader.FHIRTermGraphLoader; +import com.ibm.fhir.term.graph.loader.FHIRTermGraphLoader.Type; +import com.ibm.fhir.term.graph.loader.impl.CodeSystemTermGraphLoader; +import com.ibm.fhir.term.graph.loader.impl.SnomedTermGraphLoader; +import com.ibm.fhir.term.graph.loader.impl.UMLSTermGraphLoader; + +public class FHIRTermGraphLoaderFactory { + public static FHIRTermGraphLoader create(Type type, Map options) { + switch (type) { + case CODESYSTEM: + return new CodeSystemTermGraphLoader(options); + case SNOMED: + return new SnomedTermGraphLoader(options); + case UMLS: + return new UMLSTermGraphLoader(options); + default: + throw new IllegalArgumentException("Unknown type: " + type); + } + } +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/AbstractTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/AbstractTermGraphLoader.java new file mode 100644 index 00000000000..6d7cf6ffa79 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/AbstractTermGraphLoader.java @@ -0,0 +1,57 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.loader.impl; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; + +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; +import org.janusgraph.core.JanusGraph; + +import com.ibm.fhir.term.graph.FHIRTermGraph; +import com.ibm.fhir.term.graph.factory.FHIRTermGraphFactory; +import com.ibm.fhir.term.graph.loader.FHIRTermGraphLoader; +import com.ibm.fhir.term.graph.loader.util.LabelFilter; + +public abstract class AbstractTermGraphLoader implements FHIRTermGraphLoader { + protected final Map options; + protected final FHIRTermGraph graph; + protected final JanusGraph janusGraph; + protected final GraphTraversalSource g; + protected final LabelFilter labelFilter; + + public AbstractTermGraphLoader(Map options) { + this.options = Objects.requireNonNull(options, "options"); + + String propFileName = options.get("config"); + graph = FHIRTermGraphFactory.open(propFileName); + janusGraph = graph.getJanusGraph(); + g = graph.traversal(); + + // create label filter + if (options.containsKey("labels")) { + labelFilter = new LabelFilter(new HashSet<>(Arrays.asList(options.get("labels").split(",")))); + } else { + labelFilter = LabelFilter.ACCEPT_ALL; + } + } + + @Override + public abstract void load(); + + @Override + public final void close() { + graph.close(); + } + + @Override + public final Map options() { + return options; + } +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java new file mode 100644 index 00000000000..af43ff399b2 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java @@ -0,0 +1,205 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.loader.impl; + +import static com.ibm.fhir.term.graph.loader.util.FHIRTermGraphLoaderUtil.toMap; +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.convert; +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toLong; +import static com.ibm.fhir.term.util.CodeSystemSupport.getConcepts; +import static com.ibm.fhir.term.util.CodeSystemSupport.isCaseSensitive; + +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.logging.Logger; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.MissingOptionException; +import org.apache.commons.cli.Options; +import org.apache.tinkerpop.gremlin.structure.Vertex; + +import com.ibm.fhir.model.format.Format; +import com.ibm.fhir.model.parser.FHIRParser; +import com.ibm.fhir.model.resource.CodeSystem; +import com.ibm.fhir.model.resource.CodeSystem.Concept; +import com.ibm.fhir.model.resource.CodeSystem.Concept.Designation; +import com.ibm.fhir.model.resource.CodeSystem.Concept.Property; +import com.ibm.fhir.model.type.DateTime; +import com.ibm.fhir.model.type.Element; +import com.ibm.fhir.registry.FHIRRegistry; +import com.ibm.fhir.term.graph.FHIRTermGraph; +import com.ibm.fhir.term.graph.loader.FHIRTermGraphLoader; + +public class CodeSystemTermGraphLoader extends AbstractTermGraphLoader { + private static final Logger log = Logger.getLogger(CodeSystemTermGraphLoader.class.getName()); + + private final CodeSystem codeSystem; + private final Set concepts; + private final Map conceptVertexMap; + + private Vertex codeSystemVertex = null; + + public CodeSystemTermGraphLoader(Map options) { + super(options); + + CodeSystem codeSystem = null; + + if (options.containsKey("file")) { + // load code system from file + try (InputStream in = new FileInputStream(options.get("file"))) { + codeSystem = FHIRParser.parser(Format.JSON).parse(in); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else if (options.containsKey("url")) { + // get code system from registry + codeSystem = FHIRRegistry.getInstance().getResource(options.get("url"), CodeSystem.class); + } + + this.codeSystem = Objects.requireNonNull(codeSystem, "codeSystem"); + concepts = getConcepts(codeSystem); + conceptVertexMap = new HashMap<>(); + } + + @Override + public void load() { + createCodeSystemVertex(); + createConceptVertices(); + createEdges(); + } + + public CodeSystem getCodeSystem() { + return codeSystem; + } + + private void createCodeSystemVertex() { + java.lang.String url = codeSystem.getUrl().getValue(); + + codeSystemVertex = g.addV("CodeSystem") + .property("url", url) + .property("caseSensitive", isCaseSensitive(codeSystem)) + .next(); + + if (codeSystem.getVersion() != null) { + g.V(codeSystemVertex) + .property("version", codeSystem.getVersion().getValue()) + .next(); + } + + g.tx().commit(); + } + + private void createConceptVertices() { + for (Concept concept : concepts) { + java.lang.String code = concept.getCode().getValue(); + Vertex conceptVertex = g.addV("Concept") + .property("code", code) + .property("codeLowerCase", normalize(code)) + .next(); + + if (concept.getDisplay() != null) { + java.lang.String display = concept.getDisplay().getValue(); + g.V(conceptVertex) + .property("display", display) + .next(); + } + + for (Designation designation : concept.getDesignation()) { + Vertex designationVertex = g.addV("Designation") + .property("value", designation.getValue().getValue()) + .next(); + + if (designation.getUse() != null) { + g.V(designationVertex) + .property("use", designation.getUse().getCode().getValue()) + .next(); + } + + if (designation.getLanguage() != null) { + g.V(designationVertex) + .property("language", designation.getLanguage().getValue()) + .next(); + } + + g.V(conceptVertex).addE("designation").to(designationVertex).next(); + } + + for (Property property : concept.getProperty()) { + Element value = property.getValue(); + + java.lang.String key = "value" + value.getClass().getSimpleName(); + + Vertex propertyVertex = g.addV("Property_") + .property("code", property.getCode().getValue()) + .property(key, convert(value)) + .next(); + + if (value.is(DateTime.class)) { + g.V(propertyVertex).property("valueDateTimeLong", toLong(value.as(DateTime.class))).next(); + } + + g.V(conceptVertex).addE("property_").to(propertyVertex).next(); + } + + g.V(codeSystemVertex).addE("concept").to(conceptVertex).next(); + + conceptVertexMap.put(concept, conceptVertex); + } + + g.tx().commit(); + } + + private void createEdges() { + for (Concept concept : concepts) { + Vertex v = conceptVertexMap.get(concept); + for (Concept child : concept.getConcept()) { + Vertex w = conceptVertexMap.get(child); + g.V(w).addE(FHIRTermGraph.ISA).to(v).next(); + } + } + + g.tx().commit(); + } + + public static void main(String[] args) throws Exception { + Options options = null; + CodeSystemTermGraphLoader loader = null; + try { + long start = System.currentTimeMillis(); + + options = FHIRTermGraphLoader.Type.CODESYSTEM.options(); + + CommandLineParser parser = new DefaultParser(); + CommandLine commandLine = parser.parse(options, args); + + loader = new CodeSystemTermGraphLoader(toMap(commandLine)); + loader.load(); + + long end = System.currentTimeMillis(); + + log.info("Loading time (milliseconds): " + (end - start)); + } catch (MissingOptionException e) { + System.out.println("MissingOptionException: " + e.getMessage()); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("CodeSystemTermGraphLoader", options); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("An error occurred: " + e.getMessage()); + } finally { + if (loader != null) { + loader.close(); + } + } + } +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/SnomedTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/SnomedTermGraphLoader.java new file mode 100644 index 00000000000..278a4d30d0d --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/SnomedTermGraphLoader.java @@ -0,0 +1,319 @@ +/* + * (C) Copyright IBM Corp. 2020, 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.loader.impl; + +import static com.ibm.fhir.term.graph.loader.util.FHIRTermGraphLoaderUtil.toLabel; +import static com.ibm.fhir.term.graph.loader.util.FHIRTermGraphLoaderUtil.toMap; +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.logging.Logger; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.MissingOptionException; +import org.apache.commons.cli.Options; +import org.apache.tinkerpop.gremlin.structure.Edge; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.janusgraph.core.schema.JanusGraphManagement; + +import com.ibm.fhir.term.graph.loader.FHIRTermGraphLoader; + +public class SnomedTermGraphLoader extends AbstractTermGraphLoader { + private static final Logger log = Logger.getLogger(SnomedTermGraphLoader.class.getName()); + + private static final String PREFERRED = "900000000000548007"; + private static final String FULLY_SPECIFIED_NAME = "900000000000003001"; + + private String conceptFile = null; + private String relationshipFile = null; + private String descriptionFile = null; + private String languageRefsetFile = null; + + private AtomicInteger counter = null; + private Map vertexMap = null; + private Vertex codeSystemVertex = null; + private Set preferred = null; + + public SnomedTermGraphLoader(Map options) { + super(options); + + String baseDir = options.get("base"); + conceptFile = baseDir + "/" + options.get("concept"); + descriptionFile = baseDir + "/" + options.get("desc"); + relationshipFile = baseDir + "/" + options.get("relation"); + languageRefsetFile = baseDir + "/../Refset/Language/" + options.get("lang"); + + counter = new AtomicInteger(0); + vertexMap = new HashMap<>(250000); + preferred = new HashSet<>(500000); + } + + @Override + public void load() { + createCodeSystemVertex(); + try { + processConceptsFile(); + processLanguageRefsetFile(); + processDescriptionFile(); + processRelationshipFile(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void createCodeSystemVertex() { + codeSystemVertex = g.addV("CodeSystem") + .property("url", "http://snomed.info/sct") + .next(); + g.tx().commit(); + } + + private void processConceptsFile() throws IOException, FileNotFoundException { + // concept file + log.info("Processing concepts file..."); + try (BufferedReader reader = new BufferedReader(new FileReader(conceptFile))) { + reader.lines().skip(1).forEach(new SnomedReleaseFileConsumer() { + @Override + public void processLine(String line) { + String[] tokens = line.split("\\t"); + String id = tokens[0]; + String active = tokens[2]; + + if ("1".equals(active)) { + if (!vertexMap.containsKey(id)) { + Vertex v = g.addV("Concept") + .property("code", id) + .property("codeLowerCase", normalize(id)) + .next(); + vertexMap.put(id, v); + g.V(codeSystemVertex).addE("concept").to(v).next(); + } + } + + if ((counter.get() % 10000) == 0) { + log.info("counter: " + counter.get()); + g.tx().commit(); + } + + counter.getAndIncrement(); + } + }); + + // commit any uncommitted work + g.tx().commit(); + } + + int count = counter.get(); + g.V(codeSystemVertex).property("count", count).next(); + g.tx().commit(); + } + + private void processLanguageRefsetFile() throws IOException, FileNotFoundException { + // language refset file + log.info("Processing language refset file..."); + + try (BufferedReader reader = new BufferedReader(new FileReader(languageRefsetFile))) { + reader.lines().skip(1).forEach(new SnomedReleaseFileConsumer() { + @Override + public void processLine(String line) { + String[] tokens = line.split("\\t"); + String active = tokens[2]; + String referencedComponentId = tokens[5]; + String acceptabilityId = tokens[6]; + + if ("1".equals(active) && PREFERRED.equals(acceptabilityId)) { + preferred.add(referencedComponentId); + } + } + }); + } + } + + private void processDescriptionFile() throws IOException, FileNotFoundException { + counter.set(0); + + // description file + log.info("Processing description file..."); + try (BufferedReader reader = new BufferedReader(new FileReader(descriptionFile))) { + reader.lines().skip(1).forEach(new SnomedReleaseFileConsumer() { + @Override + public void processLine(String line) { + String[] tokens = line.split("\\t"); + String id = tokens[0]; + String active = tokens[2]; + String conceptId = tokens[4]; + String typeId = tokens[6]; + String term = tokens[7]; + + Vertex v = vertexMap.get(conceptId); + + if ("1".equals(active) && v != null) { + if (preferred.contains(id) && !FULLY_SPECIFIED_NAME.equals(typeId)) { + // preferred term + g.V(v) + .property("display", term) + .next(); + } + + Vertex w = g.addV("Designation") + .property("language", "en") + .property("use", typeId) + .property("value", term) + .next(); + + g.V(v).addE("designation").to(w).next(); + } + + if ((counter.get() % 10000) == 0) { + log.info("counter: " + counter.get()); + g.tx().commit(); + } + + counter.getAndIncrement(); + } + }); + + // commit any uncommitted work + g.tx().commit(); + } + } + + private void processRelationshipFile() throws IOException, FileNotFoundException { + counter.set(0); + + // relationship file + log.info("Processing relationship file..."); + try (BufferedReader reader = new BufferedReader(new FileReader(relationshipFile))) { + reader.lines().skip(1).forEach(new SnomedReleaseFileConsumer() { + @Override + public void processLine(String line) { + String[] tokens = line.split("\\t"); + String active = tokens[2]; + String sourceId = tokens[4]; + String destinationId = tokens[5]; + String relationshipGroup = tokens[6]; + String typeId = tokens[7]; + + if ("1".equals(active)) { + Vertex u = vertexMap.get(sourceId); + Vertex v = vertexMap.get(destinationId); + Vertex w = vertexMap.get(typeId); + + if (u != null && v != null && w != null) { + String display = (String) g.V(w).values("display").next(); + String label = toLabel(display); + + if (labelFilter.accept(label)) { + if (janusGraph.getEdgeLabel(label) == null) { + log.info("Adding label: " + label); + JanusGraphManagement management = janusGraph.openManagement(); + management.makeEdgeLabel(label).make(); + management.commit(); + } + + Edge e = g.V(u).addE(label).to(v).next(); + + if (!"0".equals(relationshipGroup)) { + g.E(e).property("group", relationshipGroup).next(); + } + } + } + } + + if ((counter.get() % 10000) == 0) { + log.info("counter: " + counter.get()); + g.tx().commit(); + } + + counter.getAndIncrement(); + } + }); + + // commit any uncommitted work + g.tx().commit(); + } + } + + private static abstract class SnomedReleaseFileConsumer implements Consumer { + private final List lines = new ArrayList<>(); + private String previousId = null; + + @Override + public void accept(String line) { + if (collect(line)) { + lines.add(line); + } else { + processLines(Collections.unmodifiableList(lines)); + lines.clear(); + lines.add(line); + } + } + + private boolean collect(String line) { + String[] fields = line.split("\\t"); + String id = fields[0]; + if (!id.equals(previousId)) { + previousId = id; + return lines.isEmpty(); + } + return true; + } + + private void processLines(List lines) { + processLine(lines.get(lines.size() - 1)); + } + + // template method + public abstract void processLine(String line); + } + + public static void main(String[] args) throws Exception { + Options options = null; + SnomedTermGraphLoader loader = null; + try { + long start = System.currentTimeMillis(); + + options = FHIRTermGraphLoader.Type.SNOMED.options(); + + CommandLineParser parser = new DefaultParser(); + CommandLine commandLine = parser.parse(options, args); + + loader = new SnomedTermGraphLoader(toMap(commandLine)); + loader.load(); + + long end = System.currentTimeMillis(); + + log.info("Loading time (milliseconds): " + (end - start)); + } catch (MissingOptionException e) { + System.out.println("MissingOptionException: " + e.getMessage()); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("SnomedTermGraphLoader", options); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("An error occurred: " + e.getMessage()); + } finally { + if (loader != null) { + loader.close(); + } + } + } +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/UMLSTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/UMLSTermGraphLoader.java similarity index 77% rename from fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/UMLSTermGraphLoader.java rename to fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/UMLSTermGraphLoader.java index 1daf9c1d7f6..20d00c95491 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/UMLSTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/UMLSTermGraphLoader.java @@ -4,10 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -package com.ibm.fhir.term.graph.loader; +package com.ibm.fhir.term.graph.loader.impl; +import static com.ibm.fhir.term.graph.loader.util.FHIRTermGraphLoaderUtil.toLabel; +import static com.ibm.fhir.term.graph.loader.util.FHIRTermGraphLoaderUtil.toMap; import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; -import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toLabel; import java.io.BufferedReader; import java.io.FileNotFoundException; @@ -33,69 +34,23 @@ import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.MissingOptionException; import org.apache.commons.cli.Options; -import org.apache.commons.cli.ParseException; -import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; import org.apache.tinkerpop.gremlin.structure.Edge; import org.apache.tinkerpop.gremlin.structure.Vertex; -import org.janusgraph.core.JanusGraph; import org.janusgraph.core.schema.JanusGraphManagement; -import com.ibm.fhir.term.graph.FHIRTermGraph; -import com.ibm.fhir.term.graph.FHIRTermGraphFactory; +import com.ibm.fhir.term.graph.loader.FHIRTermGraphLoader; /* * This class will load UMLS concepts and relationships into a JanusGraph. */ -public class UMLSTermGraphLoader { +public class UMLSTermGraphLoader extends AbstractTermGraphLoader { private static final Logger LOG = Logger.getLogger(UMLSTermGraphLoader.class.getName()); - private static final String UMLS_DELIMITER = "\\|"; - - /** - * Load UMLS data using properties provided in arguments - * - * @param args - */ - public static void main(String[] args) { - UMLSTermGraphLoader loader = null; - Options options = null; - try { - long start = System.currentTimeMillis(); + private static final String UMLS_CONCEPT_NAMES_AND_SOURCES_FILE = "MRCONSO.RRF"; + private static final String UMLS_SOURCE_INFORMATION_FILE = "MRSAB.RRF"; + private static final String UMLS_RELATED_CONCEPTS_FILE = "MRREL.RRF"; - // Parse arguments - options = new Options() - .addRequiredOption("file", null, true, "Configuration properties file") - .addRequiredOption("base", null, true, "UMLS base directory") - .addRequiredOption("conceptFile", null, true, "UMLS concept (MRCONSO) file") - .addRequiredOption("relationFile", null, true, "UMLS relationship (MRREL) file") - .addRequiredOption("sourceAttributeFile", null, true, "UMLS source attribute (MRSAB) file"); - - CommandLineParser parser = new DefaultParser(); - CommandLine commandLine = parser.parse(options, args); - - String baseDir = commandLine.getOptionValue("base"); - String relationshipFile = baseDir + "/" + commandLine.getOptionValue("relationFile"); - String conceptFile = baseDir + "/" + commandLine.getOptionValue("conceptFile"); - String sabFile = baseDir + "/" + commandLine.getOptionValue("sourceAttributeFile"); - String propFileName = commandLine.getOptionValue("file"); - - loader = new UMLSTermGraphLoader(propFileName, conceptFile, relationshipFile, sabFile); - loader.load(); - - long end = System.currentTimeMillis(); - LOG.info("Loading time (milliseconds): " + (end - start)); - } catch (MissingOptionException e) { - LOG.log(Level.SEVERE, "MissingOptionException: ", e); - HelpFormatter formatter = new HelpFormatter(); - formatter.printHelp("UMLSTermGraphLoader", options); - } catch (Exception e) { - LOG.log(Level.SEVERE, "An error occurred: " + e.getMessage()); - } finally { - if (loader != null) { - loader.close(); - } - } - } + private static final String UMLS_DELIMITER = "\\|"; // Map to track AUI to SCUI relationships, since MRREL uses AUI, but granularity of concepts used in MRCONSO is at SCUI level private Map auiToScuiMap = new ConcurrentHashMap<>(1000000); @@ -112,10 +67,6 @@ public static void main(String[] args) { // Name of file containing UMLS concept data private String conceptFile = null; - private GraphTraversalSource g = null; - private FHIRTermGraph graph = null; - private JanusGraph janusGraph = null; - // Name of file containing UMLS concept relationship data private String relationshipFile = null; @@ -131,46 +82,33 @@ public static void main(String[] args) { /** * Initialize a UMLSTermGraphLoader * - * @param propFileName - * @param conceptFile - * @param relationshipFile - * @param sourceAttributeFile - * @throws ParseException - * @throws IOException - * @throws FileNotFoundException + * @param options */ - public UMLSTermGraphLoader(String propFileName, String conceptFile, String relationshipFile, String sourceAttributeFile) { - this.conceptFile = conceptFile; - this.relationshipFile = relationshipFile; - this.sourceAttributeFile = sourceAttributeFile; - - graph = FHIRTermGraphFactory.open(propFileName); - janusGraph = graph.getJanusGraph(); - g = graph.traversal(); + public UMLSTermGraphLoader(Map options) { + super(options); + + String baseDir = options.get("base"); + conceptFile = baseDir + "/" + UMLS_CONCEPT_NAMES_AND_SOURCES_FILE; + relationshipFile = baseDir + "/" + UMLS_RELATED_CONCEPTS_FILE; + sourceAttributeFile = baseDir + "/" + UMLS_SOURCE_INFORMATION_FILE; + vertexMap = new HashMap<>(250000); } /** * Loads UMLS data into JanusGraph * - * @throws ParseException - * @throws IOException - * @throws FileNotFoundException + * @throws RuntimeException */ - public void load() throws ParseException, IOException, FileNotFoundException { - loadSourceAttributes(); - loadCaseSensitiveCodeSystems(); - loadConcepts(); - loadRelations(); - } - - /** - * Close loader, thereby closing connection to JanusGraph - */ - public void close() { - if (graph != null) { - graph.close(); - graph = null; + @Override + public void load() { + try { + loadSourceAttributes(); + loadCaseSensitiveCodeSystems(); + loadConcepts(); + loadRelations(); + } catch (Exception e) { + throw new RuntimeException(e); } } @@ -194,10 +132,6 @@ private final Vertex createCodeSystemVertex(String sab) { } - public JanusGraph getJanusGraph() { - return janusGraph; - } - /** * Loads all UMLS concept data from the provided conceptFile * @@ -223,8 +157,8 @@ private void loadConcepts() throws FileNotFoundException, IOException { String tty = tokens[12]; String str = tokens[14]; String suppress = tokens[16]; - if (!"O".equals(suppress)) { + if (!"O".equals(suppress)) { auiToScuiMap.put(aui, scui); Vertex codeSystemVertex = codeSystemVertices.computeIfAbsent(sab, s -> createCodeSystemVertex(s)); @@ -246,11 +180,8 @@ private void loadConcepts() throws FileNotFoundException, IOException { LOG.severe("Could not find SCUI in vertexMap"); } else { if (tty.equals("PT")) { // Preferred entries provide preferred name and language - String displayLowerCase = normalize(str); v.property("display", str); - v.property("displayLowerCase", displayLowerCase); v.property("language", lat); - } // add new designation Vertex w = g.addV("Designation").property("language", lat).property("value", str).next(); @@ -299,12 +230,14 @@ private void loadRelations() throws FileNotFoundException, IOException { String aui1 = tokens[1]; String rela = tokens[7]; String aui2 = tokens[5]; + String rg = tokens[12]; // relationship group String dir = tokens[13]; String suppress = tokens[14]; - if (!"N".equals(dir) && !"O".equals(suppress)) { // Don't load relations that are not in source order or suppressed + if (!"N".equals(dir) && !"O".equals(suppress)) { // Don't load relations that are not in source order or suppressed String scui1 = auiToScuiMap.get(aui1); String scui2 = auiToScuiMap.get(aui2); + if (scui1 != null && scui2 != null) { Vertex v1 = vertexMap.get(scui1); Vertex v2 = vertexMap.get(scui2); @@ -312,15 +245,20 @@ private void loadRelations() throws FileNotFoundException, IOException { if (v1 != null && v2 != null) { String label = toLabel(rela); - if (janusGraph.getEdgeLabel(label) == null) { - LOG.info("Adding label: " + label); - JanusGraphManagement management = janusGraph.openManagement(); - management.makeEdgeLabel(label).make(); - management.commit(); - } + if (labelFilter.accept(label)) { + if (janusGraph.getEdgeLabel(label) == null) { + LOG.info("Adding label: " + label); + JanusGraphManagement management = janusGraph.openManagement(); + management.makeEdgeLabel(label).make(); + management.commit(); + } + + Edge e = g.V(v2).addE(label).to(v1).next(); - Edge e = g.V(v2).addE(label).to(v1).next(); - g.E(e).next(); + if (!"".equals(rg)) { + g.E(e).property("group", rg).next(); + } + } } if ((counter.get() % 10000) == 0) { @@ -357,6 +295,7 @@ private void loadSourceAttributes() throws IOException { String rsab = tokens[3]; String sver = tokens[6]; String curver = tokens[21]; + if ("Y".equals(curver)) { if ("SNOMEDCT_US".equals(rsab)) { // special case version for SNOMED @@ -381,4 +320,38 @@ private void loadCaseSensitiveCodeSystems() throws IOException { } } } + + /** + * Load UMLS data using properties provided in arguments + * + * @param args + */ + public static void main(String[] args) { + UMLSTermGraphLoader loader = null; + Options options = null; + try { + long start = System.currentTimeMillis(); + + options = FHIRTermGraphLoader.Type.UMLS.options(); + + CommandLineParser parser = new DefaultParser(); + CommandLine commandLine = parser.parse(options, args); + + loader = new UMLSTermGraphLoader(toMap(commandLine)); + loader.load(); + + long end = System.currentTimeMillis(); + LOG.info("Loading time (milliseconds): " + (end - start)); + } catch (MissingOptionException e) { + LOG.log(Level.SEVERE, "MissingOptionException: ", e); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("UMLSTermGraphLoader", options); + } catch (Exception e) { + LOG.log(Level.SEVERE, "An error occurred: " + e.getMessage()); + } finally { + if (loader != null) { + loader.close(); + } + } + } } \ No newline at end of file diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/main/FHIRTermGraphLoaderMain.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/main/FHIRTermGraphLoaderMain.java new file mode 100644 index 00000000000..9696e7a04e7 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/main/FHIRTermGraphLoaderMain.java @@ -0,0 +1,73 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.loader.main; + +import static com.ibm.fhir.term.graph.loader.util.FHIRTermGraphLoaderUtil.toMap; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.MissingOptionException; +import org.apache.commons.cli.Options; + +import com.ibm.fhir.term.graph.loader.FHIRTermGraphLoader; +import com.ibm.fhir.term.graph.loader.factory.FHIRTermGraphLoaderFactory; + +public class FHIRTermGraphLoaderMain { + public static void main(String[] args) throws Exception { + Options options = null; + FHIRTermGraphLoader.Type type = null; + FHIRTermGraphLoader loader = null; + try { + long start = System.currentTimeMillis(); + + type = getType(args); + options = type.options(); + + // parse command line arguments using loader type specific options + CommandLineParser parser = new DefaultParser(); + CommandLine commandLine = parser.parse(options, getArguments(args, 1)); + + loader = FHIRTermGraphLoaderFactory.create(type, toMap(commandLine)); + loader.load(); + + long end = System.currentTimeMillis(); + + System.out.println("Loading time (milliseconds): " + (end - start)); + } catch (MissingOptionException e) { + System.out.println("MissingOptionException: " + e.getMessage()); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(String.format("FHIRTermGraphLoaderMain %s ", type.toString()), options); + } catch (Exception e) { + e.printStackTrace(); + System.out.println("An error occurred: " + e.getMessage()); + } finally { + if (loader != null) { + loader.close(); + } + } + } + + private static FHIRTermGraphLoader.Type getType(String[] args) { + if (args.length > 0) { + return FHIRTermGraphLoader.Type.valueOf(args[0]); + } + throw new IllegalArgumentException("loader type not found"); + } + + private static String[] getArguments(String[] args, int beginIndex) { + List arguments = new ArrayList<>(); + for (int i = beginIndex; i < args.length; i++) { + arguments.add(args[i]); + } + return arguments.toArray(new String[arguments.size()]); + } +} \ No newline at end of file diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/util/FHIRTermGraphLoaderUtil.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/util/FHIRTermGraphLoaderUtil.java new file mode 100644 index 00000000000..dbebbbbd836 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/util/FHIRTermGraphLoaderUtil.java @@ -0,0 +1,52 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.loader.util; + +import java.util.Arrays; +import java.util.Collections; +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 org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; + +import com.ibm.fhir.term.graph.FHIRTermGraph; + +public class FHIRTermGraphLoaderUtil { + private final static Set RESERVED_WORDS = new HashSet<>(Arrays.asList("key", "vertex", "edge", "element", "property", "label")); + + private FHIRTermGraphLoaderUtil() { } + + public static boolean isReservedWord(String s) { + return RESERVED_WORDS.contains(s.toLowerCase()); + } + + public static String toLabel(String typeName) { + List tokens = Arrays.asList(typeName.split(" - | |_|-")); + String label = tokens.stream() + .map(token -> token.substring(0, 1).toUpperCase() + token.substring(1)) + .collect(Collectors.joining("")); + label = label.substring(0, 1).toLowerCase() + label.substring(1); + if ("isA".equals(label)) { + // for consistency between SNOMED-CT and UMLS + return FHIRTermGraph.ISA; + } + return isReservedWord(label) ? label + "_" : label; + } + + public static Map toMap(CommandLine commandLine) { + Map map = new LinkedHashMap<>(); + for (Option option : commandLine.getOptions()) { + map.put(option.getOpt(), option.getValue()); + } + return Collections.unmodifiableMap(map); + } +} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/util/LabelFilter.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/util/LabelFilter.java new file mode 100644 index 00000000000..39015aaf7fa --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/util/LabelFilter.java @@ -0,0 +1,37 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.loader.util; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +/* + * Used to filter edge labels during graph creation + */ +public class LabelFilter { + public static final LabelFilter ACCEPT_ALL = new LabelFilter() { + @Override + public boolean accept(String label) { + return true; + } + }; + + private final Set labels; + + public LabelFilter(Set labels) { + this.labels = Objects.requireNonNull(labels, "labels"); + } + + private LabelFilter() { + this(Collections.emptySet()); + } + + public boolean accept(String label) { + return labels.contains(label); + } +} 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 b54d5548636..07e2e975bd3 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 @@ -31,10 +31,12 @@ import com.ibm.fhir.model.resource.ValueSet.Compose.Include.Filter; import com.ibm.fhir.model.type.Code; import com.ibm.fhir.model.type.Coding; +import com.ibm.fhir.model.type.DateTime; +import com.ibm.fhir.model.type.Decimal; import com.ibm.fhir.model.type.Element; import com.ibm.fhir.model.type.Uri; import com.ibm.fhir.term.graph.FHIRTermGraph; -import com.ibm.fhir.term.graph.FHIRTermGraphFactory; +import com.ibm.fhir.term.graph.factory.FHIRTermGraphFactory; import com.ibm.fhir.term.spi.FHIRTermServiceProvider; public class GraphTermServiceProvider implements FHIRTermServiceProvider { @@ -50,7 +52,7 @@ public Set closure(CodeSystem codeSystem, Code code) { Set concepts = new LinkedHashSet<>(); concepts.add(getConcept(codeSystem, code, false, false)); whereCodeSystem(hasCode(vertices(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem) - .repeat(__.in("isA") + .repeat(__.in(FHIRTermGraph.ISA) .simplePath() .dedup()) .emit() @@ -131,7 +133,7 @@ public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { return true; } return whereCodeSystem(hasCode(vertices(), codeA.getValue(), caseSensitive), codeSystem) - .repeat(__.in("isA") + .repeat(__.in(FHIRTermGraph.ISA) .simplePath()) .until(hasCode(codeB.getValue(), caseSensitive)) .hasNext(); @@ -204,35 +206,14 @@ private int getCount(CodeSystem codeSystem) { private List getDesignations(CodeSystem codeSystem, String code) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); List designations = new ArrayList<>(); - String designationUseSystem = getDesignationUseSystem(codeSystem); whereCodeSystem(hasCode(vertices(), code, isCaseSensitive(codeSystem)), codeSystem) .out("designation") .elementMap() .toStream() - .forEach(elementMap -> designations.add(createDesignation(elementMap, designationUseSystem))); + .forEach(elementMap -> designations.add(createDesignation(elementMap, codeSystem.getUrl().getValue()))); return designations; } - private String getDesignationUseSystem(CodeSystem codeSystem) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); - Optional> optional = hasVersion(hasUrl(vertices(), codeSystem.getUrl()), codeSystem.getVersion()) - .elementMap("designationUseSystem") - .tryNext(); - if (optional.isPresent()) { - return (String) optional.get().get("designationUseSystem"); - } - return null; - } - - private Element getValue(Map elementMap) { - for (Object value : elementMap.values()) { - if (value instanceof Element) { - return (Element) value; - } - } - return null; - } - private List getProperties(CodeSystem codeSystem, String code) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); List properties = new ArrayList<>(); @@ -244,6 +225,28 @@ private List getProperties(CodeSystem codeSystem, String code) { return properties; } + private Element getValue(Map elementMap) { + if (elementMap.containsKey("valueBoolean")) { + return com.ibm.fhir.model.type.Boolean.of((Boolean) elementMap.get("valueBoolean")); + } + if (elementMap.containsKey("valueCode")) { + return Code.of((String) elementMap.get("valueCode")); + } + if (elementMap.containsKey("valueDateTime")) { + return DateTime.of((String) elementMap.get("valueDateTime")); + } + if (elementMap.containsKey("valueDecimal")) { + return Decimal.of((Double) elementMap.get("valueDecimal")); + } + if (elementMap.containsKey("valueInteger")) { + return com.ibm.fhir.model.type.Integer.of((Integer) elementMap.get("valueInteger")); + } + if (elementMap.containsKey("valueString")) { + return string((String) elementMap.get("valueString")); + } + return null; + } + private GraphTraversal hasCode(GraphTraversal g, String code, boolean caseSensitive) { if (caseSensitive) { return g.has("code", code); diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/BooleanSerializer.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/BooleanSerializer.java deleted file mode 100644 index 7d1312918f4..00000000000 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/BooleanSerializer.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2021 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.ibm.fhir.term.graph.serialize; - -import org.janusgraph.core.attribute.AttributeSerializer; -import org.janusgraph.diskstorage.ScanBuffer; -import org.janusgraph.diskstorage.WriteBuffer; - -import com.ibm.fhir.model.type.Boolean; - -public class BooleanSerializer implements AttributeSerializer { - @Override - public Boolean read(ScanBuffer buffer) { - return Boolean.of(buffer.getBoolean()); - } - - @Override - public void write(WriteBuffer buffer, Boolean attribute) { - buffer.putBoolean(attribute.getValue()); - } -} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/CodeSerializer.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/CodeSerializer.java deleted file mode 100644 index 69d870ea499..00000000000 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/CodeSerializer.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2021 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.ibm.fhir.term.graph.serialize; - -import org.janusgraph.core.attribute.AttributeSerializer; -import org.janusgraph.diskstorage.ScanBuffer; -import org.janusgraph.diskstorage.WriteBuffer; -import org.janusgraph.graphdb.database.serialize.attribute.StringSerializer; - -import com.ibm.fhir.model.type.Code; - -public class CodeSerializer implements AttributeSerializer { - private final StringSerializer serializer; - - public CodeSerializer() { - serializer = new StringSerializer(); - } - - @Override - public Code read(ScanBuffer buffer) { - return Code.of(serializer.read(buffer)); - } - - @Override - public void write(WriteBuffer buffer, Code attribute) { - serializer.write(buffer, attribute.getValue()); - } -} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DateTimeSerializer.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DateTimeSerializer.java deleted file mode 100644 index 6aee119c5e4..00000000000 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DateTimeSerializer.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2021 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.ibm.fhir.term.graph.serialize; - -import org.janusgraph.core.attribute.AttributeSerializer; -import org.janusgraph.diskstorage.ScanBuffer; -import org.janusgraph.diskstorage.WriteBuffer; -import org.janusgraph.graphdb.database.serialize.attribute.StringSerializer; -import com.ibm.fhir.model.type.DateTime; - -public class DateTimeSerializer implements AttributeSerializer { - private final StringSerializer serializer; - - public DateTimeSerializer() { - serializer = new StringSerializer(); - } - - @Override - public DateTime read(ScanBuffer buffer) { - return DateTime.of(serializer.read(buffer)); - } - - @Override - public void write(WriteBuffer buffer, DateTime attribute) { - serializer.write(buffer, DateTime.PARSER_FORMATTER.format(attribute.getValue())); - } -} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DecimalSerializer.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DecimalSerializer.java deleted file mode 100644 index 5782a278dc7..00000000000 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/DecimalSerializer.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2021 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.ibm.fhir.term.graph.serialize; - -import org.janusgraph.core.attribute.AttributeSerializer; -import org.janusgraph.diskstorage.ScanBuffer; -import org.janusgraph.diskstorage.WriteBuffer; - -import com.ibm.fhir.model.type.Decimal; - -public class DecimalSerializer implements AttributeSerializer { - @Override - public Decimal read(ScanBuffer buffer) { - return Decimal.of(buffer.getDouble()); - } - - @Override - public void write(WriteBuffer buffer, Decimal attribute) { - buffer.putDouble(attribute.getValue().doubleValue()); - } -} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/IntegerSerializer.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/IntegerSerializer.java deleted file mode 100644 index 4747164f940..00000000000 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/IntegerSerializer.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2021 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.ibm.fhir.term.graph.serialize; - -import org.janusgraph.core.attribute.AttributeSerializer; -import org.janusgraph.diskstorage.ScanBuffer; -import org.janusgraph.diskstorage.WriteBuffer; - -import com.ibm.fhir.model.type.Integer; - -public class IntegerSerializer implements AttributeSerializer { - @Override - public Integer read(ScanBuffer buffer) { - return Integer.of(buffer.getInt()); - } - - @Override - public void write(WriteBuffer buffer, Integer attribute) { - buffer.putInt(attribute.getValue()); - } -} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/StringSerializer.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/StringSerializer.java deleted file mode 100644 index f1806a914cf..00000000000 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/serialize/StringSerializer.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2021 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.ibm.fhir.term.graph.serialize; - -import static com.ibm.fhir.model.type.String.string; - -import org.janusgraph.core.attribute.AttributeSerializer; -import org.janusgraph.diskstorage.ScanBuffer; -import org.janusgraph.diskstorage.WriteBuffer; - -import com.ibm.fhir.model.type.String; - -public class StringSerializer implements AttributeSerializer { - private final org.janusgraph.graphdb.database.serialize.attribute.StringSerializer serializer; - - public StringSerializer() { - serializer = new org.janusgraph.graphdb.database.serialize.attribute.StringSerializer(); - } - - @Override - public String read(ScanBuffer buffer) { - return string(serializer.read(buffer)); - } - - @Override - public void write(WriteBuffer buffer, String attribute) { - serializer.write(buffer, attribute.getValue()); - } -} diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java index dd649799c47..9b6be94672d 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java @@ -6,26 +6,52 @@ package com.ibm.fhir.term.graph.util; +import static com.ibm.fhir.model.util.ModelSupport.FHIR_BOOLEAN; +import static com.ibm.fhir.model.util.ModelSupport.FHIR_INTEGER; +import static com.ibm.fhir.model.util.ModelSupport.FHIR_STRING; + import java.text.Normalizer; import java.text.Normalizer.Form; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; +import java.time.LocalDate; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAccessor; import org.slf4j.LoggerFactory; +import com.ibm.fhir.model.type.Code; +import com.ibm.fhir.model.type.DateTime; +import com.ibm.fhir.model.type.Decimal; +import com.ibm.fhir.model.type.Element; + import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; public class FHIRTermGraphUtil { - private final static Set RESERVED_WORDS = new HashSet<>(Arrays.asList("key", "vertex", "edge", "element", "property", "label")); - private FHIRTermGraphUtil() { } - public static boolean isReservedWord(String s) { - return RESERVED_WORDS.contains(s.toLowerCase()); + public static Object convert(Element value) { + if (value.is(FHIR_BOOLEAN)) { + return value.as(FHIR_BOOLEAN).getValue(); + } + if (value.is(Code.class)) { + return value.as(Code.class).getValue(); + } + if (value.is(DateTime.class)) { + return DateTime.PARSER_FORMATTER.format(value.as(DateTime.class).getValue()); + } + if (value.is(Decimal.class)) { + return value.as(Decimal.class).getValue().doubleValue(); + } + if (value.is(FHIR_INTEGER)) { + return value.as(FHIR_INTEGER).getValue(); + } + if (value.is(FHIR_STRING)) { + return value.as(FHIR_STRING).getValue(); + } + throw new IllegalArgumentException(); } public static String normalize(String value) { @@ -40,12 +66,24 @@ public static void setRootLoggerLevel(Level level) { rootLogger.setLevel(level); } - public static String toLabel(String typeName) { - List tokens = Arrays.asList(typeName.split(" - | |_|-")); - String label = tokens.stream() - .map(token -> token.substring(0, 1).toUpperCase() + token.substring(1)) - .collect(Collectors.joining("")); - label = label.substring(0, 1).toLowerCase() + label.substring(1); - return isReservedWord(label) ? label + "_" : label; + public static Long toLong(DateTime dateTime) { + TemporalAccessor value = dateTime.getValue(); + if (value instanceof ZonedDateTime) { + ZonedDateTime zonedDateTime = (ZonedDateTime) value; + return zonedDateTime.toInstant().toEpochMilli(); + } + if (value instanceof LocalDate) { + LocalDate localDate = (LocalDate) value; + return localDate.atStartOfDay().atZone(ZoneOffset.UTC).toInstant().toEpochMilli(); + } + if (value instanceof YearMonth) { + YearMonth yearMonth = (YearMonth) value; + return yearMonth.atDay(1).atStartOfDay().atZone(ZoneOffset.UTC).toInstant().toEpochMilli(); + } + if (value instanceof Year) { + Year year = (Year) value; + return year.atMonth(1).atDay(1).atStartOfDay().atZone(ZoneOffset.UTC).toInstant().toEpochMilli(); + } + throw new IllegalArgumentException(); } } diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java index b43a9c9b237..135d11a602d 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java @@ -14,23 +14,15 @@ import com.ibm.fhir.model.parser.FHIRParser; import com.ibm.fhir.model.resource.CodeSystem; import com.ibm.fhir.model.type.Code; -import com.ibm.fhir.term.graph.FHIRTermGraph; -import com.ibm.fhir.term.graph.FHIRTermGraphFactory; -import com.ibm.fhir.term.graph.loader.CodeSystemTermGraphLoader; import com.ibm.fhir.term.graph.provider.GraphTermServiceProvider; public class CodeSystemTermGraphLoaderTest { public static void main(String[] args) throws Exception { - try (InputStream in = CodeSystemTermGraphLoaderTest.class.getClassLoader().getResourceAsStream("JSON/CodeSystem-cs5.json")) { - CodeSystem codeSystem = FHIRParser.parser(Format.JSON).parse(in); - FHIRTermGraph termGraph = FHIRTermGraphFactory.open("conf/janusgraph-berkeleyje-lucene.properties"); - CodeSystemTermGraphLoader loader = new CodeSystemTermGraphLoader(termGraph, codeSystem); - loader.load(); - - GraphTermServiceProvider provider = new GraphTermServiceProvider(new PropertiesConfiguration("conf/local-graph.properties")); - System.out.println(provider.subsumes(codeSystem, Code.of("m"), Code.of("p"))); - System.out.println(provider.getConcept(codeSystem, Code.of("o"))); - provider.getGraph().close(); - } + InputStream in = CodeSystemTermGraphLoaderTest.class.getClassLoader().getResourceAsStream("JSON/CodeSystem-cs5.json"); + CodeSystem codeSystem = FHIRParser.parser(Format.JSON).parse(in); + GraphTermServiceProvider provider = new GraphTermServiceProvider(new PropertiesConfiguration("conf/janusgraph-berkeleyje-lucene.properties")); + System.out.println(provider.subsumes(codeSystem, Code.of("m"), Code.of("p"))); + System.out.println(provider.getConcept(codeSystem, Code.of("o"))); + provider.getGraph().close(); } } diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java index eda4d3b24dd..e0eb4baa4a5 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java @@ -6,6 +6,8 @@ package com.ibm.fhir.term.graph.test; +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.convert; + import java.util.stream.Stream; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; @@ -15,7 +17,7 @@ import com.ibm.fhir.model.type.Code; import com.ibm.fhir.model.type.Decimal; import com.ibm.fhir.term.graph.FHIRTermGraph; -import com.ibm.fhir.term.graph.FHIRTermGraphFactory; +import com.ibm.fhir.term.graph.factory.FHIRTermGraphFactory; public class FHIRTermGraphTest { public static void main(String[] args) throws Exception { @@ -33,29 +35,29 @@ public static void main(String[] args) throws Exception { Vertex v3 = g.addV("Concept").property("code", "c").next(); System.out.println(v3.id()); - g.V(v1).addE("isA").from(v2).next(); - g.V(v2).addE("isA").from(v3).next(); + g.V(v1).addE(FHIRTermGraph.ISA).from(v2).next(); + g.V(v2).addE(FHIRTermGraph.ISA).from(v3).next(); Stream.concat( g.V(v1) .elementMap() .toStream(), g.V(v1) - .repeat(__.in("isA").simplePath()) + .repeat(__.in(FHIRTermGraph.ISA).simplePath()) .emit() .elementMap() .toStream()) .forEach(System.out::println); - g.V(v1).property("valueCode", Code.of("someCode")).next(); - g.V(v1).property("valueDecimal", Decimal.of(100.0)).next(); - g.V(v1).property("valueInteger", com.ibm.fhir.model.type.Integer.of(0)).next(); + g.V(v1).property("valueCode", convert(Code.of("someCode"))).next(); + g.V(v1).property("valueDecimal", convert(Decimal.of(100.0))).next(); + g.V(v1).property("valueInteger", convert(com.ibm.fhir.model.type.Integer.of(0))).next(); g.tx().commit(); - System.out.println(g.V().has("valueCode", Code.of("someCode")).hasNext()); - System.out.println(g.V().has("valueDecimal", Decimal.of(100)).hasNext()); - System.out.println(g.V().has("valueInteger", com.ibm.fhir.model.type.Integer.of(-0)).hasNext()); + System.out.println(g.V().has("valueCode", convert(Code.of("someCode"))).hasNext()); + System.out.println(g.V().has("valueDecimal", convert(Decimal.of(100))).hasNext()); + System.out.println(g.V().has("valueInteger", convert(com.ibm.fhir.model.type.Integer.of(-0))).hasNext()); System.out.println(Integer.valueOf(0).equals(Integer.valueOf(-0))); From e65dad61ebc76e848ede75a4e515944d7fe550eb Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Mon, 8 Mar 2021 15:30:19 -0500 Subject: [PATCH 10/50] Issue #1980 - updates Signed-off-by: John T.E. Timm --- .../ibm/fhir/term/graph/FHIRTermGraph.java | 2 +- .../term/graph/impl/FHIRTermGraphImpl.java | 4 +- .../impl/CodeSystemTermGraphLoader.java | 16 +- .../loader/util/FHIRTermGraphLoaderUtil.java | 2 +- .../provider/GraphTermServiceProvider.java | 156 +++++++++++++++++- .../term/graph/util/FHIRTermGraphUtil.java | 2 +- .../term/graph/test/FHIRTermGraphTest.java | 96 +++++++++-- .../ibm/fhir/term/util/CodeSystemSupport.java | 86 ++++++---- 8 files changed, 303 insertions(+), 61 deletions(-) diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java index fbb111535b9..b42292a166b 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java @@ -18,7 +18,7 @@ * A graph that represents FHIR CodeSystem content and is backed by a graph database (Janusgraph) */ public interface FHIRTermGraph { - public static final String ISA = "isa"; + public static final String IS_A = "isa"; Configuration configuration(); JanusGraph getJanusGraph(); GraphTraversalSource traversal(); diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java index 77bf2d28dde..fa6151b9064 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java @@ -101,7 +101,7 @@ private void createSchema(JanusGraph graph) { management.makeVertexLabel("Property_").make(); // edge labels - management.makeEdgeLabel(FHIRTermGraph.ISA).make(); + management.makeEdgeLabel(FHIRTermGraph.IS_A).make(); management.makeEdgeLabel("concept").make(); management.makeEdgeLabel("designation").make(); management.makeEdgeLabel("property_").make(); @@ -123,7 +123,7 @@ private void createSchema(JanusGraph graph) { management.buildIndex("byValueString", Vertex.class).addKey(valueString).buildCompositeIndex(); // mixed indexes - management.buildIndex("vertices", Vertex.class).addKey(display).addKey(value).buildMixedIndex("search"); + management.buildIndex("vertices", Vertex.class).addKey(display).addKey(value).addKey(valueString).buildMixedIndex("search"); log.info(System.lineSeparator() + management.printSchema()); diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java index af43ff399b2..b4bfd2a555f 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java @@ -7,7 +7,7 @@ package com.ibm.fhir.term.graph.loader.impl; import static com.ibm.fhir.term.graph.loader.util.FHIRTermGraphLoaderUtil.toMap; -import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.convert; +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toObject; import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toLong; import static com.ibm.fhir.term.util.CodeSystemSupport.getConcepts; @@ -50,7 +50,7 @@ public class CodeSystemTermGraphLoader extends AbstractTermGraphLoader { private Vertex codeSystemVertex = null; - public CodeSystemTermGraphLoader(Map options) { + public CodeSystemTermGraphLoader(Map options) { super(options); CodeSystem codeSystem = null; @@ -84,7 +84,7 @@ public CodeSystem getCodeSystem() { } private void createCodeSystemVertex() { - java.lang.String url = codeSystem.getUrl().getValue(); + String url = codeSystem.getUrl().getValue(); codeSystemVertex = g.addV("CodeSystem") .property("url", url) @@ -102,14 +102,14 @@ private void createCodeSystemVertex() { private void createConceptVertices() { for (Concept concept : concepts) { - java.lang.String code = concept.getCode().getValue(); + String code = concept.getCode().getValue(); Vertex conceptVertex = g.addV("Concept") .property("code", code) .property("codeLowerCase", normalize(code)) .next(); if (concept.getDisplay() != null) { - java.lang.String display = concept.getDisplay().getValue(); + String display = concept.getDisplay().getValue(); g.V(conceptVertex) .property("display", display) .next(); @@ -138,11 +138,11 @@ private void createConceptVertices() { for (Property property : concept.getProperty()) { Element value = property.getValue(); - java.lang.String key = "value" + value.getClass().getSimpleName(); + String key = "value" + value.getClass().getSimpleName(); Vertex propertyVertex = g.addV("Property_") .property("code", property.getCode().getValue()) - .property(key, convert(value)) + .property(key, toObject(value)) .next(); if (value.is(DateTime.class)) { @@ -165,7 +165,7 @@ private void createEdges() { Vertex v = conceptVertexMap.get(concept); for (Concept child : concept.getConcept()) { Vertex w = conceptVertexMap.get(child); - g.V(w).addE(FHIRTermGraph.ISA).to(v).next(); + g.V(w).addE(FHIRTermGraph.IS_A).to(v).next(); } } diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/util/FHIRTermGraphLoaderUtil.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/util/FHIRTermGraphLoaderUtil.java index dbebbbbd836..70349bf1159 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/util/FHIRTermGraphLoaderUtil.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/util/FHIRTermGraphLoaderUtil.java @@ -37,7 +37,7 @@ public static String toLabel(String typeName) { label = label.substring(0, 1).toLowerCase() + label.substring(1); if ("isA".equals(label)) { // for consistency between SNOMED-CT and UMLS - return FHIRTermGraph.ISA; + return FHIRTermGraph.IS_A; } return isReservedWord(label) ? label + "_" : label; } 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 07e2e975bd3..137ceb5f643 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 @@ -8,9 +8,16 @@ import static com.ibm.fhir.model.type.String.string; import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toLong; +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toObject; +import static com.ibm.fhir.term.util.CodeSystemSupport.convertsToBoolean; +import static com.ibm.fhir.term.util.CodeSystemSupport.getCodeSystemPropertyType; +import static com.ibm.fhir.term.util.CodeSystemSupport.hasCodeSystemProperty; import static com.ibm.fhir.term.util.CodeSystemSupport.isCaseSensitive; +import static com.ibm.fhir.term.util.CodeSystemSupport.toElement; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; @@ -18,11 +25,14 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import org.apache.commons.configuration.Configuration; +import org.apache.tinkerpop.gremlin.process.traversal.P; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.janusgraph.core.attribute.Text; import com.ibm.fhir.model.resource.CodeSystem; import com.ibm.fhir.model.resource.CodeSystem.Concept; @@ -35,8 +45,11 @@ import com.ibm.fhir.model.type.Decimal; import com.ibm.fhir.model.type.Element; import com.ibm.fhir.model.type.Uri; +import com.ibm.fhir.model.type.code.CodeSystemHierarchyMeaning; +import com.ibm.fhir.model.type.code.PropertyType; import com.ibm.fhir.term.graph.FHIRTermGraph; import com.ibm.fhir.term.graph.factory.FHIRTermGraphFactory; +import com.ibm.fhir.term.graph.util.FHIRTermGraphUtil; import com.ibm.fhir.term.spi.FHIRTermServiceProvider; public class GraphTermServiceProvider implements FHIRTermServiceProvider { @@ -52,7 +65,7 @@ public Set closure(CodeSystem codeSystem, Code code) { Set concepts = new LinkedHashSet<>(); concepts.add(getConcept(codeSystem, code, false, false)); whereCodeSystem(hasCode(vertices(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem) - .repeat(__.in(FHIRTermGraph.ISA) + .repeat(__.in(FHIRTermGraph.IS_A) .simplePath() .dedup()) .emit() @@ -80,34 +93,149 @@ public Set getConcepts(CodeSystem codeSystem) { return Collections.emptySet(); } + @SuppressWarnings("unchecked") @Override public Set getConcepts(CodeSystem codeSystem, List filters) { + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + + Set concepts = new LinkedHashSet<>(); + + GraphTraversal g = vertices(); + + /* + // TODO: make the time limit configurable + GraphTraversal g = vertices().timeLimit(30000L); + TimeLimitStep timeLimitStep = (TimeLimitStep) g.asAdmin().getEndStep(); + */ + + boolean caseSensitive = isCaseSensitive(codeSystem); + for (Filter filter : filters) { + Code property = filter.getProperty(); + com.ibm.fhir.model.type.String value = filter.getValue(); + switch (filter.getOp().getValueAsEnumConstant()) { case DESCENDENT_OF: + // descendants + if ("concept".equals(property.getValue()) && + CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + g = hasCode(g, value.getValue(), caseSensitive) + .repeat(__.in(FHIRTermGraph.IS_A) + .simplePath() + .dedup()) + .emit(); + } break; case EQUALS: + PropertyType type = getCodeSystemPropertyType(codeSystem, property); + if ("parent".equals(property.getValue()) || + "child".equals(property.getValue()) || + (hasCodeSystemProperty(codeSystem, property) && !PropertyType.CODING.equals(type))) { + if ("parent".equals(property.getValue())) { + g = hasCode(g, value.getValue(), caseSensitive).in(FHIRTermGraph.IS_A); + } else if ("child".equals(property.getValue())) { + g = hasCode(g, value.getValue(), caseSensitive).out(FHIRTermGraph.IS_A); + } else { + String propertyKey = getPropertyKey(type); + Element element = toElement(value, type); + if (element.is(DateTime.class)) { + g = g.has(propertyKey, toLong(element.as(DateTime.class))); + } else { + g = g.has(propertyKey, toObject(element)); + } + } + } break; case EXISTS: + if (hasCodeSystemProperty(codeSystem, property) && convertsToBoolean(value)) { + if (Boolean.valueOf(value.getValue())) { + g = g.has("code", property.getValue()).in("property_"); + } else { + g = g.hasNot("code").in("property_"); + } + } break; case GENERALIZES: + // ancestors and self + if ("concept".equals(property.getValue()) && + CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + g = hasCode(g, value.getValue(), caseSensitive) + .union(__.identity(), hasCode(g, value.getValue(), caseSensitive) + .repeat(__.out(FHIRTermGraph.IS_A) + .simplePath() + .dedup()) + .emit()); + } break; case IN: + if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { + if (caseSensitive) { + g = g.has("code", P.within(Arrays.stream(value.getValue().split(",")) + .collect(Collectors.toSet()))); + } else { + g = g.has("codeLowerCase", P.within(Arrays.stream(value.getValue().split(",")) + .map(FHIRTermGraphUtil::normalize) + .collect(Collectors.toSet()))); + } + } break; case IS_A: + // descendants and self + if ("concept".equals(property.getValue()) && + CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + g = hasCode(g, value.getValue(), caseSensitive) + .union(__.identity(), hasCode(g, value.getValue(), caseSensitive) + .repeat(__.in(FHIRTermGraph.IS_A) + .simplePath() + .dedup()) + .emit()); + } break; case IS_NOT_A: + // not descendants or self + if ("concept".equals(property.getValue()) && + CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + g = g.not(__.repeat(__.out(FHIRTermGraph.IS_A).simplePath()) + .until(hasCode(value.getValue(), caseSensitive))) + .not(hasCode(value.getValue(), caseSensitive)) + .hasLabel("Concept"); + } break; case NOT_IN: + if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { + if (caseSensitive) { + g = g.has("code", P.without(Arrays.stream(value.getValue().split(",")) + .collect(Collectors.toSet()))); + } else { + g = g.has("codeLowerCase", P.without(Arrays.stream(value.getValue().split(",")) + .map(FHIRTermGraphUtil::normalize) + .collect(Collectors.toSet()))); + } + } break; case REGEX: + if (hasCodeSystemProperty(codeSystem, property) && PropertyType.STRING.equals(getCodeSystemPropertyType(codeSystem, property))) { + g = g.has("valueString", Text.textContainsRegex(value.getValue())); + } break; default: break; } } - throw new UnsupportedOperationException(); + + whereCodeSystem(g, codeSystem) + .elementMap() + .toStream() + .forEach(elementMap -> concepts.add(createConcept(elementMap))); + + /* + if (timeLimitStep.getTimedOut()) { + // TODO: throw new timed out exception + } + */ + + return concepts; } public FHIRTermGraph getGraph() { @@ -133,7 +261,7 @@ public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { return true; } return whereCodeSystem(hasCode(vertices(), codeA.getValue(), caseSensitive), codeSystem) - .repeat(__.in(FHIRTermGraph.ISA) + .repeat(__.in(FHIRTermGraph.IS_A) .simplePath()) .until(hasCode(codeB.getValue(), caseSensitive)) .hasNext(); @@ -225,6 +353,26 @@ private List getProperties(CodeSystem codeSystem, String code) { return properties; } + private String getPropertyKey(PropertyType type) { + switch (type.getValueAsEnumConstant()) { + case BOOLEAN: + return "valueBoolean"; + case CODE: + return "valueCode"; +// case CODING: + case DATE_TIME: + return "valueDateTimeLong"; + case DECIMAL: + return "valueDecimal"; + case INTEGER: + return "valueInteger"; + case STRING: + return "valueString"; + default: + return null; + } + } + private Element getValue(Map elementMap) { if (elementMap.containsKey("valueBoolean")) { return com.ibm.fhir.model.type.Boolean.of((Boolean) elementMap.get("valueBoolean")); @@ -255,7 +403,7 @@ private GraphTraversal hasCode(GraphTraversal g, } // anonymous graph traversal - private GraphTraversal hasCode(String code, boolean caseSensitive) { + private GraphTraversal hasCode(String code, boolean caseSensitive) { if (caseSensitive) { return __.has("code", code); } diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java index 9b6be94672d..edfc7a1858f 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java @@ -32,7 +32,7 @@ public class FHIRTermGraphUtil { private FHIRTermGraphUtil() { } - public static Object convert(Element value) { + public static Object toObject(Element value) { if (value.is(FHIR_BOOLEAN)) { return value.as(FHIR_BOOLEAN).getValue(); } diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java index e0eb4baa4a5..83bd852442f 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java @@ -6,12 +6,14 @@ package com.ibm.fhir.term.graph.test; -import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.convert; +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toObject; import java.util.stream.Stream; +import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.TimeLimitStep; import org.apache.tinkerpop.gremlin.structure.Vertex; import com.ibm.fhir.model.type.Code; @@ -20,6 +22,7 @@ import com.ibm.fhir.term.graph.factory.FHIRTermGraphFactory; public class FHIRTermGraphTest { + @SuppressWarnings({ "unchecked", "rawtypes" }) public static void main(String[] args) throws Exception { FHIRTermGraph graph = FHIRTermGraphFactory.open("conf/janusgraph-berkeleyje-lucene.properties"); @@ -34,32 +37,101 @@ public static void main(String[] args) throws Exception { System.out.println(v2.id()); Vertex v3 = g.addV("Concept").property("code", "c").next(); System.out.println(v3.id()); + Vertex v4 = g.addV("Property_").next(); + System.out.println(v4.id()); - g.V(v1).addE(FHIRTermGraph.ISA).from(v2).next(); - g.V(v2).addE(FHIRTermGraph.ISA).from(v3).next(); + g.V(v1).addE(FHIRTermGraph.IS_A).from(v2).next(); + g.V(v2).addE(FHIRTermGraph.IS_A).from(v3).next(); + g.V(v4).property("valueCode", toObject(Code.of("someCode"))).next(); + g.V(v4).property("valueDecimal", toObject(Decimal.of(100.0))).next(); + g.V(v4).property("valueInteger", toObject(com.ibm.fhir.model.type.Integer.of(0))).next(); + + g.tx().commit(); + + System.out.println(g.V().has("valueCode", toObject(Code.of("someCode"))).hasNext()); + System.out.println(g.V().has("valueDecimal", toObject(Decimal.of(100))).hasNext()); + System.out.println(g.V().has("valueInteger", toObject(com.ibm.fhir.model.type.Integer.of(-0))).hasNext()); + System.out.println(Integer.valueOf(0).equals(Integer.valueOf(-0))); + + System.out.println(""); + + // Descendants of 'a' and self (using stream concatenation) Stream.concat( g.V(v1) .elementMap() .toStream(), g.V(v1) - .repeat(__.in(FHIRTermGraph.ISA).simplePath()) + .repeat(__.in(FHIRTermGraph.IS_A).simplePath()) .emit() .elementMap() .toStream()) .forEach(System.out::println); - g.V(v1).property("valueCode", convert(Code.of("someCode"))).next(); - g.V(v1).property("valueDecimal", convert(Decimal.of(100.0))).next(); - g.V(v1).property("valueInteger", convert(com.ibm.fhir.model.type.Integer.of(0))).next(); + System.out.println(""); - g.tx().commit(); + // Descendants of 'a' and self + g.V().has("code", "a").union(__.identity(), g.V().has("code", "a") + .repeat(__.in("isa") + .simplePath() + .dedup()) + .emit()) + .elementMap() + .toStream() + .forEach(System.out::println); - System.out.println(g.V().has("valueCode", convert(Code.of("someCode"))).hasNext()); - System.out.println(g.V().has("valueDecimal", convert(Decimal.of(100))).hasNext()); - System.out.println(g.V().has("valueInteger", convert(com.ibm.fhir.model.type.Integer.of(-0))).hasNext()); + System.out.println(""); - System.out.println(Integer.valueOf(0).equals(Integer.valueOf(-0))); + // Descendants of 'b' and self + g.V().has("code", "b").union(__.identity(), + g.V().has("code", "b") + .repeat(__.in("isa") + .simplePath() + .dedup()) + .emit()) + .elementMap() + .toStream() + .forEach(System.out::println); + + System.out.println(""); + + // Union of 'a', 'b', and 'c' + g.V().has("code", "a").union(__.identity(), + g.V().has("code", "b"), + g.V().has("code", "c")) + .elementMap() + .toStream() + .forEach(System.out::println); + + System.out.println(""); + + // Descendants of 'b' + g.V().filter(__.repeat(__.out("isa")).until(__.has("code", "b"))).elementMap().toStream().forEach(System.out::println); + + System.out.println(""); + + // Not descendants of 'b' + + GraphTraversal graphTraversal = g.V(); + + graphTraversal = graphTraversal.timeLimit(100L); + + TimeLimitStep timeLimitStep = (TimeLimitStep) graphTraversal.asAdmin().getEndStep(); + + graphTraversal.not(__.repeat(__.out("isa")).until(__.has("code", "b"))).hasLabel("Concept").elementMap().toStream().forEach(System.out::println); + + System.out.println(timeLimitStep.getTimedOut()); + + System.out.println(""); + + // Not descendants of 'b' or self + g.V().not(__.repeat(__.out("isa")).until(__.has("code", "b"))).not(__.has("code", "b")).hasLabel("Concept").elementMap().toStream().forEach(System.out::println); + + System.out.println(""); + + g.V().not(__.until(__.has("code", "b")).repeat((GraphTraversal) __.out("isa"))).hasLabel("Concept").elementMap().toStream().forEach(System.out::println); + + System.out.println(""); graph.close(); } 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 1382a1a6c46..f2b950c6876 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 @@ -25,13 +25,13 @@ 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.CodeableConcept; import com.ibm.fhir.model.type.DateTime; import com.ibm.fhir.model.type.Decimal; import com.ibm.fhir.model.type.Element; import com.ibm.fhir.model.type.Integer; import com.ibm.fhir.model.type.String; import com.ibm.fhir.model.type.code.CodeSystemHierarchyMeaning; +import com.ibm.fhir.model.type.code.PropertyType; import com.ibm.fhir.registry.FHIRRegistry; /** @@ -44,6 +44,10 @@ public final class CodeSystemSupport { private CodeSystemSupport() { } + public static boolean convertsToBoolean(String value) { + return "true".equals(value.getValue()) || "false".equals(value.getValue()); + } + /** * Find the concept in the provided code system that matches the specified code. * @@ -179,6 +183,24 @@ public static CodeSystem.Property getCodeSystemProperty(CodeSystem codeSystem, C return null; } + /** + * Get the type of the code system property that matches the specified code. + * + * @param codeSystem + * the code system + * @param code + * the property code to match + * @return + * the type of the code system property that matches the specified code, or null if no such property exists + */ + public static PropertyType getCodeSystemPropertyType(CodeSystem codeSystem, Code code) { + CodeSystem.Property property = getCodeSystemProperty(codeSystem, code); + if (property != null) { + return property.getType(); + } + return null; + } + /** * Get the concept property that matches the specified code. * @@ -273,6 +295,30 @@ public static Set getConcepts(Concept concept) { return concepts; } + public static Boolean toBoolean(String value) { + return "true".equals(value.getValue()) ? Boolean.TRUE : Boolean.FALSE; + } + + public static Element toElement(String value, PropertyType type) { + switch (type.getValueAsEnumConstant()) { + case BOOLEAN: + return Boolean.of(value.getValue()); + case CODE: + return Code.of(value.getValue()); +// case CODING: + case DATE_TIME: + return DateTime.of(value.getValue()); + case DECIMAL: + return Decimal.of(value.getValue()); + case INTEGER: + return Integer.of(value.getValue()); + case STRING: + return value; + default: + return null; + } + } + private static boolean accept(List conceptFilters, Concept concept) { for (ConceptFilter conceptFilter : conceptFilters) { if (!conceptFilter.accept(concept)) { @@ -328,33 +374,6 @@ private static Code code(String value) { return Code.of(value.getValue()); } - private static Element convert(String value, Class targetType) { - if (Code.class.equals(targetType)) { - return Code.of(value.getValue()); - } - if (Integer.class.equals(targetType)) { - return Integer.of(value.getValue()); - } - if (Boolean.class.equals(targetType)) { - return Boolean.of(value.getValue()); - } - if (DateTime.class.equals(targetType)) { - return DateTime.of(value.getValue()); - } - if (Decimal.class.equals(targetType)) { - return Decimal.of(value.getValue()); - } - return value; - } - - private static boolean convertsToBoolean(String value) { - return "true".equals(value.getValue()) || "false".equals(value.getValue()); - } - - private static Boolean toBoolean(String value) { - return "true".equals(value.getValue()) ? Boolean.TRUE : Boolean.FALSE; - } - private static ConceptFilter createDescendentOfFilter(CodeSystem codeSystem, Filter filter) { if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { Concept concept = findConcept(codeSystem, code(filter.getValue())); @@ -369,7 +388,7 @@ private static ConceptFilter createEqualsFilter(CodeSystem codeSystem, Filter fi Code property = filter.getProperty(); if ("parent".equals(property.getValue()) || "child".equals(property.getValue()) || - hasCodeSystemProperty(codeSystem, property)) { + (hasCodeSystemProperty(codeSystem, property) && !PropertyType.CODING.equals(getCodeSystemPropertyType(codeSystem, property)))) { return new EqualsFilter(codeSystem, property, filter.getValue()); } return null; @@ -436,7 +455,8 @@ private static ConceptFilter createNotInFilter(CodeSystem codeSystem, Filter fil private static ConceptFilter createRegexFilter(CodeSystem codeSystem, Filter filter) { Code property = filter.getProperty(); - if (hasCodeSystemProperty(codeSystem, property)) { + if (hasCodeSystemProperty(codeSystem, property) && + PropertyType.STRING.equals(getCodeSystemPropertyType(codeSystem, property))) { return new RegexFilter(property, filter.getValue()); } return null; @@ -462,12 +482,14 @@ public boolean accept(Concept concept) { } private static class EqualsFilter implements ConceptFilter { + private final PropertyType type; private final Code property; private final String value; private final Set children; private final Concept child; public EqualsFilter(CodeSystem codeSystem, Code property, String value) { + this.type = getCodeSystemPropertyType(codeSystem, property); this.property = property; this.value = value; children = new LinkedHashSet<>(); @@ -490,8 +512,8 @@ public boolean accept(Concept concept) { } if (hasConceptProperty(concept, property)) { Element value = getConceptPropertyValue(concept, property); - if (value != null && !value.is(CodeableConcept.class)) { - return value.equals(convert(this.value, value.getClass())); + if (value != null) { + return value.equals(toElement(this.value, type)); } } return false; From 9e914eeaa803613bcf5b3038ce18804cde9a083d Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Mon, 8 Mar 2021 16:57:11 -0500 Subject: [PATCH 11/50] Issue #1980 - fixed incompatible type issue Signed-off-by: John T.E. Timm --- .../provider/GraphTermServiceProvider.java | 67 +++++++++---------- .../term/graph/test/FHIRTermGraphTest.java | 2 +- .../ibm/fhir/term/util/CodeSystemSupport.java | 6 +- 3 files changed, 35 insertions(+), 40 deletions(-) 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 137ceb5f643..ab7c5fe7c16 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 @@ -100,13 +100,12 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { Set concepts = new LinkedHashSet<>(); - GraphTraversal g = vertices(); /* // TODO: make the time limit configurable GraphTraversal g = vertices().timeLimit(30000L); - TimeLimitStep timeLimitStep = (TimeLimitStep) g.asAdmin().getEndStep(); + TimeLimitStep timeLimitStep = (TimeLimitStep) g.asAdmin().getEndStep(); */ boolean caseSensitive = isCaseSensitive(codeSystem); @@ -118,9 +117,8 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { switch (filter.getOp().getValueAsEnumConstant()) { case DESCENDENT_OF: // descendants - if ("concept".equals(property.getValue()) && - CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - g = hasCode(g, value.getValue(), caseSensitive) + if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) .repeat(__.in(FHIRTermGraph.IS_A) .simplePath() .dedup()) @@ -129,20 +127,19 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { break; case EQUALS: PropertyType type = getCodeSystemPropertyType(codeSystem, property); - if ("parent".equals(property.getValue()) || - "child".equals(property.getValue()) || - (hasCodeSystemProperty(codeSystem, property) && !PropertyType.CODING.equals(type))) { + if ((("parent".equals(property.getValue()) || "child".equals(property.getValue())) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) + || (hasCodeSystemProperty(codeSystem, property) && !PropertyType.CODING.equals(type))) { if ("parent".equals(property.getValue())) { - g = hasCode(g, value.getValue(), caseSensitive).in(FHIRTermGraph.IS_A); + g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem).in(FHIRTermGraph.IS_A); } else if ("child".equals(property.getValue())) { - g = hasCode(g, value.getValue(), caseSensitive).out(FHIRTermGraph.IS_A); + g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem).out(FHIRTermGraph.IS_A); } else { String propertyKey = getPropertyKey(type); Element element = toElement(value, type); if (element.is(DateTime.class)) { - g = g.has(propertyKey, toLong(element.as(DateTime.class))); + g = whereCodeSystem(g.has(propertyKey, toLong(element.as(DateTime.class))), codeSystem); } else { - g = g.has(propertyKey, toObject(element)); + g = whereCodeSystem(g.has(propertyKey, toObject(element)), codeSystem); } } } @@ -150,18 +147,17 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { case EXISTS: if (hasCodeSystemProperty(codeSystem, property) && convertsToBoolean(value)) { if (Boolean.valueOf(value.getValue())) { - g = g.has("code", property.getValue()).in("property_"); + g = whereCodeSystem(g.has("code", property.getValue()).in("property_"), codeSystem); } else { - g = g.hasNot("code").in("property_"); + g = whereCodeSystem(g.hasNot("code").in("property_"), codeSystem); } } break; case GENERALIZES: // ancestors and self - if ("concept".equals(property.getValue()) && - CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - g = hasCode(g, value.getValue(), caseSensitive) - .union(__.identity(), hasCode(g, value.getValue(), caseSensitive) + if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) + .union(__.identity(), whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) .repeat(__.out(FHIRTermGraph.IS_A) .simplePath() .dedup()) @@ -171,21 +167,20 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { case IN: if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { if (caseSensitive) { - g = g.has("code", P.within(Arrays.stream(value.getValue().split(",")) - .collect(Collectors.toSet()))); + g = whereCodeSystem(g.has("code", P.within(Arrays.stream(value.getValue().split(",")) + .collect(Collectors.toSet()))), codeSystem); } else { - g = g.has("codeLowerCase", P.within(Arrays.stream(value.getValue().split(",")) + g = whereCodeSystem(g.has("codeLowerCase", P.within(Arrays.stream(value.getValue().split(",")) .map(FHIRTermGraphUtil::normalize) - .collect(Collectors.toSet()))); + .collect(Collectors.toSet()))), codeSystem); } } break; case IS_A: // descendants and self - if ("concept".equals(property.getValue()) && - CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - g = hasCode(g, value.getValue(), caseSensitive) - .union(__.identity(), hasCode(g, value.getValue(), caseSensitive) + if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) + .union(__.identity(), whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) .repeat(__.in(FHIRTermGraph.IS_A) .simplePath() .dedup()) @@ -194,29 +189,28 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { break; case IS_NOT_A: // not descendants or self - if ("concept".equals(property.getValue()) && - CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - g = g.not(__.repeat(__.out(FHIRTermGraph.IS_A).simplePath()) + if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + g = whereCodeSystem(g.not(__.repeat(__.out(FHIRTermGraph.IS_A).simplePath()) .until(hasCode(value.getValue(), caseSensitive))) .not(hasCode(value.getValue(), caseSensitive)) - .hasLabel("Concept"); + .hasLabel("Concept"), codeSystem); } break; case NOT_IN: if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { if (caseSensitive) { - g = g.has("code", P.without(Arrays.stream(value.getValue().split(",")) - .collect(Collectors.toSet()))); + g = whereCodeSystem(g.has("code", P.without(Arrays.stream(value.getValue().split(",")) + .collect(Collectors.toSet()))), codeSystem); } else { - g = g.has("codeLowerCase", P.without(Arrays.stream(value.getValue().split(",")) + g = whereCodeSystem(g.has("codeLowerCase", P.without(Arrays.stream(value.getValue().split(",")) .map(FHIRTermGraphUtil::normalize) - .collect(Collectors.toSet()))); + .collect(Collectors.toSet()))), codeSystem); } } break; case REGEX: if (hasCodeSystemProperty(codeSystem, property) && PropertyType.STRING.equals(getCodeSystemPropertyType(codeSystem, property))) { - g = g.has("valueString", Text.textContainsRegex(value.getValue())); + g = whereCodeSystem(g.has("valueString", Text.textContainsRegex(value.getValue())), codeSystem); } break; default: @@ -224,10 +218,13 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { } } + /* whereCodeSystem(g, codeSystem) .elementMap() .toStream() .forEach(elementMap -> concepts.add(createConcept(elementMap))); + */ + g.elementMap().toStream().forEach(elementMap -> concepts.add(createConcept(elementMap))); /* if (timeLimitStep.getTimedOut()) { diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java index 83bd852442f..5bb24d7b156 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java @@ -116,7 +116,7 @@ public static void main(String[] args) throws Exception { graphTraversal = graphTraversal.timeLimit(100L); - TimeLimitStep timeLimitStep = (TimeLimitStep) graphTraversal.asAdmin().getEndStep(); + TimeLimitStep timeLimitStep = (TimeLimitStep) graphTraversal.asAdmin().getEndStep(); graphTraversal.not(__.repeat(__.out("isa")).until(__.has("code", "b"))).hasLabel("Concept").elementMap().toStream().forEach(System.out::println); 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 f2b950c6876..73afe724f2a 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 @@ -386,8 +386,7 @@ private static ConceptFilter createDescendentOfFilter(CodeSystem codeSystem, Fil private static ConceptFilter createEqualsFilter(CodeSystem codeSystem, Filter filter) { Code property = filter.getProperty(); - if ("parent".equals(property.getValue()) || - "child".equals(property.getValue()) || + if ((("parent".equals(property.getValue()) || "child".equals(property.getValue())) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) || (hasCodeSystemProperty(codeSystem, property) && !PropertyType.CODING.equals(getCodeSystemPropertyType(codeSystem, property)))) { return new EqualsFilter(codeSystem, property, filter.getValue()); } @@ -455,8 +454,7 @@ private static ConceptFilter createNotInFilter(CodeSystem codeSystem, Filter fil private static ConceptFilter createRegexFilter(CodeSystem codeSystem, Filter filter) { Code property = filter.getProperty(); - if (hasCodeSystemProperty(codeSystem, property) && - PropertyType.STRING.equals(getCodeSystemPropertyType(codeSystem, property))) { + if (hasCodeSystemProperty(codeSystem, property) && PropertyType.STRING.equals(getCodeSystemPropertyType(codeSystem, property))) { return new RegexFilter(property, filter.getValue()); } return null; From 9bac1afd4819fa030e908f428e6953cdebeac1a7 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Wed, 10 Mar 2021 11:57:13 -0500 Subject: [PATCH 12/50] Issue #1980 - updates Signed-off-by: John T.E. Timm --- .../ibm/fhir/term/graph/FHIRTermGraph.java | 1 + .../term/graph/impl/FHIRTermGraphImpl.java | 9 +++ .../graph/loader/FHIRTermGraphLoader.java | 3 + .../loader/impl/AbstractTermGraphLoader.java | 41 +++++++++++-- .../impl/CodeSystemTermGraphLoader.java | 23 +++++++- .../provider/GraphTermServiceProvider.java | 27 ++++----- .../test/CodeSystemTermGraphLoaderTest.java | 58 ++++++++++++++++--- .../term/graph/test/FHIRTermGraphTest.java | 2 +- .../ibm/fhir/term/util/CodeSystemSupport.java | 40 ++++++++----- 9 files changed, 158 insertions(+), 46 deletions(-) diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java index b42292a166b..90329e9548d 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java @@ -28,4 +28,5 @@ default Stream> indexQuery(String query) { Stream> indexQuery(String query, int limit); void close(); void drop(); + void dropAllVertices(); } diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java index fa6151b9064..7a01ff53904 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java @@ -194,4 +194,13 @@ public void drop() { graph = null; } } + + @Override + public void dropAllVertices() { + log.info("Dropping all vertices..."); + if (traversal != null) { + traversal.V().drop().iterate(); + traversal.tx().commit(); + } + } } diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java index d5da686b99b..f6b1561eaab 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java @@ -10,6 +10,8 @@ import org.apache.commons.cli.Options; +import com.ibm.fhir.term.graph.FHIRTermGraph; + public interface FHIRTermGraphLoader { enum Type { CODESYSTEM { @@ -49,4 +51,5 @@ public Options options() { void load(); void close(); Map options(); + FHIRTermGraph getGraph(); } diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/AbstractTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/AbstractTermGraphLoader.java index 6d7cf6ffa79..5273502c78e 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/AbstractTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/AbstractTermGraphLoader.java @@ -11,6 +11,7 @@ import java.util.Map; import java.util.Objects; +import org.apache.commons.configuration.Configuration; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource; import org.janusgraph.core.JanusGraph; @@ -35,11 +36,30 @@ public AbstractTermGraphLoader(Map options) { g = graph.traversal(); // create label filter - if (options.containsKey("labels")) { - labelFilter = new LabelFilter(new HashSet<>(Arrays.asList(options.get("labels").split(",")))); - } else { - labelFilter = LabelFilter.ACCEPT_ALL; - } + labelFilter = createLabelFilter(options); + } + + public AbstractTermGraphLoader(Map options, Configuration configuration) { + this.options = Objects.requireNonNull(options, "options"); + Objects.requireNonNull(configuration, "configuration"); + + graph = FHIRTermGraphFactory.open(configuration); + janusGraph = graph.getJanusGraph(); + g = graph.traversal(); + + // create label filter + labelFilter = createLabelFilter(options); + } + + public AbstractTermGraphLoader(Map options, FHIRTermGraph graph) { + this.options = Objects.requireNonNull(options, "options"); + + this.graph = Objects.requireNonNull(graph, "graph"); + janusGraph = graph.getJanusGraph(); + g = graph.traversal(); + + // create label filter + labelFilter = createLabelFilter(options); } @Override @@ -54,4 +74,15 @@ public final void close() { public final Map options() { return options; } + + @Override + public final FHIRTermGraph getGraph() { + return graph; + } + + protected LabelFilter createLabelFilter(Map options) { + return options.containsKey("labels") ? + new LabelFilter(new HashSet<>(Arrays.asList(options.get("labels").split(",")))) : + LabelFilter.ACCEPT_ALL; + } } diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java index b4bfd2a555f..45a1d58cd9d 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java @@ -7,14 +7,14 @@ package com.ibm.fhir.term.graph.loader.impl; import static com.ibm.fhir.term.graph.loader.util.FHIRTermGraphLoaderUtil.toMap; -import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toObject; import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toLong; +import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toObject; import static com.ibm.fhir.term.util.CodeSystemSupport.getConcepts; -import static com.ibm.fhir.term.util.CodeSystemSupport.isCaseSensitive; import java.io.FileInputStream; import java.io.InputStream; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -27,6 +27,7 @@ import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.MissingOptionException; import org.apache.commons.cli.Options; +import org.apache.commons.configuration.Configuration; import org.apache.tinkerpop.gremlin.structure.Vertex; import com.ibm.fhir.model.format.Format; @@ -72,6 +73,22 @@ public CodeSystemTermGraphLoader(Map options) { conceptVertexMap = new HashMap<>(); } + public CodeSystemTermGraphLoader(Configuration configuration, CodeSystem codeSystem) { + super(Collections.emptyMap(), configuration); + + this.codeSystem = Objects.requireNonNull(codeSystem, "codeSystem"); + concepts = getConcepts(codeSystem); + conceptVertexMap = new HashMap<>(); + } + + public CodeSystemTermGraphLoader(FHIRTermGraph graph, CodeSystem codeSystem) { + super(Collections.emptyMap(), graph); + + this.codeSystem = Objects.requireNonNull(codeSystem, "codeSystem"); + concepts = getConcepts(codeSystem); + conceptVertexMap = new HashMap<>(); + } + @Override public void load() { createCodeSystemVertex(); @@ -88,7 +105,7 @@ private void createCodeSystemVertex() { codeSystemVertex = g.addV("CodeSystem") .property("url", url) - .property("caseSensitive", isCaseSensitive(codeSystem)) + .property("count", concepts.size()) .next(); if (codeSystem.getVersion() != null) { 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 ab7c5fe7c16..75f4f2f0f8b 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 @@ -53,12 +53,18 @@ import com.ibm.fhir.term.spi.FHIRTermServiceProvider; public class GraphTermServiceProvider implements FHIRTermServiceProvider { + private static final int DEFAULT_COUNT = 1000; private final FHIRTermGraph graph; public GraphTermServiceProvider(Configuration configuration) { + Objects.requireNonNull(configuration, "configuration"); graph = FHIRTermGraphFactory.open(configuration); } + public GraphTermServiceProvider(FHIRTermGraph graph) { + this.graph = Objects.requireNonNull(graph, "graph"); + } + @Override public Set closure(CodeSystem codeSystem, Code code) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); @@ -84,13 +90,13 @@ public Concept getConcept(CodeSystem codeSystem, Code code) { @Override public Set getConcepts(CodeSystem codeSystem) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); - List concepts = new ArrayList<>(getCount(codeSystem)); + Set concepts = new LinkedHashSet<>(getCount(codeSystem)); hasVersion(hasUrl(vertices(), codeSystem.getUrl()), codeSystem.getVersion()) .out("concept") .elementMap() .toStream() .forEach(elementMap -> concepts.add(createConcept(elementMap))); - return Collections.emptySet(); + return concepts; } @SuppressWarnings("unchecked") @@ -325,7 +331,7 @@ private int getCount(CodeSystem codeSystem) { if (optional.isPresent()) { return (Integer) optional.get().get("count"); } - return -1; + return DEFAULT_COUNT; } private List getDesignations(CodeSystem codeSystem, String code) { @@ -393,18 +399,12 @@ private Element getValue(Map elementMap) { } private GraphTraversal hasCode(GraphTraversal g, String code, boolean caseSensitive) { - if (caseSensitive) { - return g.has("code", code); - } - return g.has("codeLowerCase", normalize(code)); + return caseSensitive ? g.has("code", code) : g.has("codeLowerCase", normalize(code)); } // anonymous graph traversal private GraphTraversal hasCode(String code, boolean caseSensitive) { - if (caseSensitive) { - return __.has("code", code); - } - return __.has("codeLowerCase", normalize(code)); + return caseSensitive ? __.has("code", code) : __.has("codeLowerCase", normalize(code)); } private GraphTraversal hasUrl(GraphTraversal g, Uri url) { @@ -412,10 +412,7 @@ private GraphTraversal hasUrl(GraphTraversal g, } private GraphTraversal hasVersion(GraphTraversal g, com.ibm.fhir.model.type.String version) { - if (version != null) { - return g.has("version", version.getValue()); - } - return g; + return (version != null) ? g.has("version", version.getValue()) : g; } private GraphTraversal vertices(Object... vertexIds) { diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java index 135d11a602d..8ac1842335f 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java @@ -7,22 +7,62 @@ package com.ibm.fhir.term.graph.test; import java.io.InputStream; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; import org.apache.commons.configuration.PropertiesConfiguration; +import org.locationtech.jts.util.Assert; +import org.testng.annotations.Test; import com.ibm.fhir.model.format.Format; import com.ibm.fhir.model.parser.FHIRParser; import com.ibm.fhir.model.resource.CodeSystem; -import com.ibm.fhir.model.type.Code; +import com.ibm.fhir.model.resource.CodeSystem.Concept; +import com.ibm.fhir.term.graph.FHIRTermGraph; +import com.ibm.fhir.term.graph.factory.FHIRTermGraphFactory; +import com.ibm.fhir.term.graph.loader.FHIRTermGraphLoader; +import com.ibm.fhir.term.graph.loader.impl.CodeSystemTermGraphLoader; import com.ibm.fhir.term.graph.provider.GraphTermServiceProvider; +import com.ibm.fhir.term.graph.util.FHIRTermGraphUtil; +import com.ibm.fhir.term.spi.FHIRTermServiceProvider; +import com.ibm.fhir.term.util.CodeSystemSupport; + +import ch.qos.logback.classic.Level; public class CodeSystemTermGraphLoaderTest { - public static void main(String[] args) throws Exception { - InputStream in = CodeSystemTermGraphLoaderTest.class.getClassLoader().getResourceAsStream("JSON/CodeSystem-cs5.json"); - CodeSystem codeSystem = FHIRParser.parser(Format.JSON).parse(in); - GraphTermServiceProvider provider = new GraphTermServiceProvider(new PropertiesConfiguration("conf/janusgraph-berkeleyje-lucene.properties")); - System.out.println(provider.subsumes(codeSystem, Code.of("m"), Code.of("p"))); - System.out.println(provider.getConcept(codeSystem, Code.of("o"))); - provider.getGraph().close(); + @Test + public void testCodeSystemTermGraphLoader() throws Exception { + FHIRTermGraphUtil.setRootLoggerLevel(Level.INFO); + + FHIRTermGraph graph = null; + try (InputStream in = CodeSystemTermGraphLoaderTest.class.getClassLoader().getResourceAsStream("JSON/CodeSystem-cs5.json")) { + graph = FHIRTermGraphFactory.open(new PropertiesConfiguration("conf/janusgraph-berkeleyje-lucene.properties")); + graph.dropAllVertices(); + + CodeSystem codeSystem = FHIRParser.parser(Format.JSON).parse(in); + FHIRTermGraphLoader loader = new CodeSystemTermGraphLoader(graph, codeSystem); + loader.load(); + + FHIRTermServiceProvider provider = new GraphTermServiceProvider(graph); + + Set expected = new LinkedHashSet<>(); + for (Concept concept : CodeSystemSupport.getConcepts(codeSystem)) { + expected.add(concept.toBuilder() + .concept(Collections.emptyList()) + .build()); + } + + Set actual = new LinkedHashSet<>(); + for (Concept concept : provider.getConcepts(codeSystem)) { + actual.add(provider.getConcept(codeSystem, concept.getCode())); + } + + Assert.equals(expected, actual); + } finally { + if (graph != null) { + graph.close(); + } + } } -} +} \ No newline at end of file diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java index 5bb24d7b156..25d99873259 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java @@ -29,7 +29,7 @@ public static void main(String[] args) throws Exception { GraphTraversalSource g = graph.traversal(); g.V().drop().iterate(); - g.E().drop().iterate(); +// g.E().drop().iterate(); Vertex v1 = g.addV("Concept").property("code", "a").next(); System.out.println(v1.id()); 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 73afe724f2a..90b4377b485 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 @@ -22,7 +22,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.Filter; +import com.ibm.fhir.model.resource.ValueSet.Compose.Include; import com.ibm.fhir.model.type.Boolean; import com.ibm.fhir.model.type.Code; import com.ibm.fhir.model.type.DateTime; @@ -31,6 +31,7 @@ import com.ibm.fhir.model.type.Integer; import com.ibm.fhir.model.type.String; import com.ibm.fhir.model.type.code.CodeSystemHierarchyMeaning; +import com.ibm.fhir.model.type.code.FilterOperator; import com.ibm.fhir.model.type.code.PropertyType; import com.ibm.fhir.registry.FHIRRegistry; @@ -95,6 +96,10 @@ public static Concept findConcept(CodeSystem codeSystem, Concept concept, Code c return result; } + 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. @@ -164,6 +169,15 @@ public static CodeSystem getCodeSystem(java.lang.String url) { return FHIRRegistry.getInstance().getResource(url, CodeSystem.class); } + public static CodeSystem.Filter getCodeSystemFilter(CodeSystem codeSystem, Code code, FilterOperator operator) { + for (CodeSystem.Filter filter : codeSystem.getFilter()) { + if (filter.getCode().equals(code) && filter.getOperator().contains(operator)) { + return filter; + } + } + return null; + } + /** * Get the code system property that matches the specified code. * @@ -263,7 +277,7 @@ public static Set getConcepts(CodeSystem codeSystem) { * @return * flattened / filtered list of Concept instances for the given code system */ - public static Set getConcepts(CodeSystem codeSystem, List filters) { + public static Set getConcepts(CodeSystem codeSystem, List filters) { Set concepts = new LinkedHashSet<>(); List conceptFilters = buildConceptFilters(codeSystem, filters); for (Concept concept : getConcepts(codeSystem)) { @@ -328,9 +342,9 @@ private static boolean accept(List conceptFilters, Concept concep return true; } - private static List buildConceptFilters(CodeSystem codeSystem, List filters) { + private static List buildConceptFilters(CodeSystem codeSystem, List filters) { List conceptFilters = new ArrayList<>(filters.size()); - for (Filter filter : filters) { + for (Include.Filter filter : filters) { ConceptFilter conceptFilter = null; switch (filter.getOp().getValueAsEnumConstant()) { case DESCENDENT_OF: @@ -374,7 +388,7 @@ private static Code code(String value) { return Code.of(value.getValue()); } - private static ConceptFilter createDescendentOfFilter(CodeSystem codeSystem, Filter filter) { + 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())); if (concept != null) { @@ -384,7 +398,7 @@ private static ConceptFilter createDescendentOfFilter(CodeSystem codeSystem, Fil return null; } - private static ConceptFilter createEqualsFilter(CodeSystem codeSystem, Filter filter) { + private static ConceptFilter createEqualsFilter(CodeSystem codeSystem, Include.Filter filter) { Code property = filter.getProperty(); if ((("parent".equals(property.getValue()) || "child".equals(property.getValue())) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) || (hasCodeSystemProperty(codeSystem, property) && !PropertyType.CODING.equals(getCodeSystemPropertyType(codeSystem, property)))) { @@ -393,7 +407,7 @@ private static ConceptFilter createEqualsFilter(CodeSystem codeSystem, Filter fi return null; } - private static ConceptFilter createExistsFilter(CodeSystem codeSystem, Filter filter) { + private static ConceptFilter createExistsFilter(CodeSystem codeSystem, Include.Filter filter) { Code property = filter.getProperty(); String value = filter.getValue(); if (hasCodeSystemProperty(codeSystem, property) && convertsToBoolean(value)) { @@ -402,7 +416,7 @@ private static ConceptFilter createExistsFilter(CodeSystem codeSystem, Filter fi return null; } - private static ConceptFilter createGeneralizesFilter(CodeSystem codeSystem, Filter filter) { + private static ConceptFilter createGeneralizesFilter(CodeSystem codeSystem, Include.Filter filter) { if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { Concept concept = findConcept(codeSystem, code(filter.getValue())); if (concept != null) { @@ -412,7 +426,7 @@ private static ConceptFilter createGeneralizesFilter(CodeSystem codeSystem, Filt return null; } - private static ConceptFilter createInFilter(CodeSystem codeSystem, Filter filter) { + private static ConceptFilter createInFilter(CodeSystem codeSystem, Include.Filter filter) { Code property = filter.getProperty(); if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { return new InFilter(property, Arrays.asList(filter.getValue().getValue().split(",")).stream() @@ -422,7 +436,7 @@ private static ConceptFilter createInFilter(CodeSystem codeSystem, Filter filter return null; } - private static ConceptFilter createIsAFilter(CodeSystem codeSystem, Filter filter) { + private static ConceptFilter createIsAFilter(CodeSystem codeSystem, Include.Filter filter) { if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { Concept concept = findConcept(codeSystem, code(filter.getValue())); if (concept != null) { @@ -432,7 +446,7 @@ private static ConceptFilter createIsAFilter(CodeSystem codeSystem, Filter filte return null; } - private static ConceptFilter createIsNotAFilter(CodeSystem codeSystem, Filter filter) { + private static ConceptFilter createIsNotAFilter(CodeSystem codeSystem, Include.Filter filter) { if ("concept".equals(filter.getProperty().getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { Concept concept = findConcept(codeSystem, code(filter.getValue())); if (concept != null) { @@ -442,7 +456,7 @@ private static ConceptFilter createIsNotAFilter(CodeSystem codeSystem, Filter fi return null; } - private static ConceptFilter createNotInFilter(CodeSystem codeSystem, Filter filter) { + private static ConceptFilter createNotInFilter(CodeSystem codeSystem, Include.Filter filter) { Code property = filter.getProperty(); if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { return new NotInFilter(property, Arrays.asList(filter.getValue().getValue().split(",")).stream() @@ -452,7 +466,7 @@ private static ConceptFilter createNotInFilter(CodeSystem codeSystem, Filter fil return null; } - private static ConceptFilter createRegexFilter(CodeSystem codeSystem, Filter filter) { + private static ConceptFilter createRegexFilter(CodeSystem codeSystem, Include.Filter filter) { Code property = filter.getProperty(); if (hasCodeSystemProperty(codeSystem, property) && PropertyType.STRING.equals(getCodeSystemPropertyType(codeSystem, property))) { return new RegexFilter(property, filter.getValue()); From 13a5f48e3e8cfcaedf377ec64c5b7913633dbfbb Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Thu, 11 Mar 2021 12:19:39 -0500 Subject: [PATCH 13/50] Issue #1980 - unit tests Signed-off-by: John T.E. Timm --- .../term/graph/impl/FHIRTermGraphImpl.java | 14 +- .../impl/CodeSystemTermGraphLoader.java | 3 + .../provider/GraphTermServiceProvider.java | 57 +- .../test/CodeSystemTermGraphLoaderTest.java | 16 +- .../test/GraphTermServiceProviderTest.java | 573 +++++++++++++++++- .../SnomedGraphTermServiceProviderTest.java | 55 ++ .../test/resources/JSON/CodeSystem-cs5.json | 79 --- .../test/resources/JSON/CodeSystem-test.json | 211 +++++++ .../ibm/fhir/term/util/CodeSystemSupport.java | 24 +- 9 files changed, 891 insertions(+), 141 deletions(-) create mode 100644 fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedGraphTermServiceProviderTest.java delete mode 100644 fhir-term-graph/src/test/resources/JSON/CodeSystem-cs5.json create mode 100644 fhir-term-graph/src/test/resources/JSON/CodeSystem-test.json diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java index 7a01ff53904..873551953a6 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java @@ -23,6 +23,7 @@ import org.janusgraph.core.PropertyKey; import org.janusgraph.core.RelationType; import org.janusgraph.core.schema.JanusGraphManagement; +import org.janusgraph.core.schema.Mapping; import com.ibm.fhir.term.graph.FHIRTermGraph; @@ -81,7 +82,7 @@ private void createSchema(JanusGraph graph) { PropertyKey valueBoolean = management.makePropertyKey("valueBoolean").dataType(Boolean.class).make(); PropertyKey valueCode = management.makePropertyKey("valueCode").dataType(String.class).make(); - PropertyKey valueDateTime = management.makePropertyKey("valueDateTime").dataType(String.class).make(); +// PropertyKey valueDateTime = management.makePropertyKey("valueDateTime").dataType(String.class).make(); PropertyKey valueDateTimeLong = management.makePropertyKey("valueDateTimeLong").dataType(Long.class).make(); PropertyKey valueDecimal = management.makePropertyKey("valueDecimal").dataType(Double.class).make(); PropertyKey valueInteger = management.makePropertyKey("valueInteger").dataType(Integer.class).make(); @@ -93,6 +94,8 @@ private void createSchema(JanusGraph graph) { management.makePropertyKey("language").dataType(String.class).make(); management.makePropertyKey("system").dataType(String.class).make(); management.makePropertyKey("use").dataType(String.class).make(); + management.makePropertyKey("valueDateTime").dataType(String.class).make(); + management.makePropertyKey("valueDecimalString").dataType(String.class).make(); // vertex labels management.makeVertexLabel("CodeSystem").make(); @@ -116,14 +119,19 @@ private void createSchema(JanusGraph graph) { management.buildIndex("byValueBoolean", Vertex.class).addKey(valueBoolean).buildCompositeIndex(); management.buildIndex("byValueCode", Vertex.class).addKey(valueCode).buildCompositeIndex(); - management.buildIndex("byValueDateTime", Vertex.class).addKey(valueDateTime).buildCompositeIndex(); +// management.buildIndex("byValueDateTime", Vertex.class).addKey(valueDateTime).buildCompositeIndex(); management.buildIndex("byValueDateTimeLong", Vertex.class).addKey(valueDateTimeLong).buildCompositeIndex(); management.buildIndex("byValueDecimal", Vertex.class).addKey(valueDecimal).buildCompositeIndex(); management.buildIndex("byValueInteger", Vertex.class).addKey(valueInteger).buildCompositeIndex(); management.buildIndex("byValueString", Vertex.class).addKey(valueString).buildCompositeIndex(); // mixed indexes - management.buildIndex("vertices", Vertex.class).addKey(display).addKey(value).addKey(valueString).buildMixedIndex("search"); + management.buildIndex("vertices", Vertex.class) + .addKey(display, Mapping.STRING.asParameter()) + .addKey(value, Mapping.STRING.asParameter()) + .addKey(valueString, Mapping.STRING.asParameter()) + .addKey(valueCode, Mapping.STRING.asParameter()) + .buildMixedIndex("search"); log.info(System.lineSeparator() + management.printSchema()); diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java index 45a1d58cd9d..61f2c5bc3e0 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java @@ -37,6 +37,7 @@ import com.ibm.fhir.model.resource.CodeSystem.Concept.Designation; import com.ibm.fhir.model.resource.CodeSystem.Concept.Property; import com.ibm.fhir.model.type.DateTime; +import com.ibm.fhir.model.type.Decimal; import com.ibm.fhir.model.type.Element; import com.ibm.fhir.registry.FHIRRegistry; import com.ibm.fhir.term.graph.FHIRTermGraph; @@ -164,6 +165,8 @@ private void createConceptVertices() { if (value.is(DateTime.class)) { g.V(propertyVertex).property("valueDateTimeLong", toLong(value.as(DateTime.class))).next(); + } else if (value.is(Decimal.class)) { + g.V(propertyVertex).property("valueDecimalString", value.as(Decimal.class).getValue().toPlainString()).next(); } g.V(conceptVertex).addE("property_").to(propertyVertex).next(); 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 75f4f2f0f8b..4b958a529c1 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 @@ -119,6 +119,7 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { for (Filter filter : filters) { Code property = filter.getProperty(); com.ibm.fhir.model.type.String value = filter.getValue(); + PropertyType type = getCodeSystemPropertyType(codeSystem, property); switch (filter.getOp().getValueAsEnumConstant()) { case DESCENDENT_OF: @@ -132,7 +133,6 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { } break; case EQUALS: - PropertyType type = getCodeSystemPropertyType(codeSystem, property); if ((("parent".equals(property.getValue()) || "child".equals(property.getValue())) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) || (hasCodeSystemProperty(codeSystem, property) && !PropertyType.CODING.equals(type))) { if ("parent".equals(property.getValue())) { @@ -140,13 +140,8 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { } else if ("child".equals(property.getValue())) { g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem).out(FHIRTermGraph.IS_A); } else { - String propertyKey = getPropertyKey(type); Element element = toElement(value, type); - if (element.is(DateTime.class)) { - g = whereCodeSystem(g.has(propertyKey, toLong(element.as(DateTime.class))), codeSystem); - } else { - g = whereCodeSystem(g.has(propertyKey, toObject(element)), codeSystem); - } + g = whereCodeSystem(g.has(getPropertyKey(type), element.is(DateTime.class) ? toLong(element.as(DateTime.class)) : toObject(element)).in("property_"), codeSystem); } } break; @@ -155,7 +150,7 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { if (Boolean.valueOf(value.getValue())) { g = whereCodeSystem(g.has("code", property.getValue()).in("property_"), codeSystem); } else { - g = whereCodeSystem(g.hasNot("code").in("property_"), codeSystem); + g = whereCodeSystem(g.not(__.out("property_").has("code", property.getValue())), codeSystem); } } break; @@ -163,7 +158,7 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { // ancestors and self if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) - .union(__.identity(), whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) + .union(__.identity(), whereCodeSystem(hasCode(vertices(), value.getValue(), caseSensitive), codeSystem) .repeat(__.out(FHIRTermGraph.IS_A) .simplePath() .dedup()) @@ -172,13 +167,20 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { break; case IN: if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { - if (caseSensitive) { - g = whereCodeSystem(g.has("code", P.within(Arrays.stream(value.getValue().split(",")) - .collect(Collectors.toSet()))), codeSystem); + if ("concept".equals(property.getValue())) { + if (caseSensitive) { + g = whereCodeSystem(g.has("code", P.within(Arrays.stream(value.getValue().split(",")) + .collect(Collectors.toSet()))), codeSystem); + } else { + g = whereCodeSystem(g.has("codeLowerCase", P.within(Arrays.stream(value.getValue().split(",")) + .map(FHIRTermGraphUtil::normalize) + .collect(Collectors.toSet()))), codeSystem); + } } else { - g = whereCodeSystem(g.has("codeLowerCase", P.within(Arrays.stream(value.getValue().split(",")) - .map(FHIRTermGraphUtil::normalize) - .collect(Collectors.toSet()))), codeSystem); + g = whereCodeSystem(g.has(getPropertyKey(type), P.within(Arrays.stream(value.getValue().split(",")) + .map(v -> toElement(v, type)) + .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) + .collect(Collectors.toSet()))).in("property_"), codeSystem); } } break; @@ -186,7 +188,7 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { // descendants and self if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) - .union(__.identity(), whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) + .union(__.identity(), whereCodeSystem(hasCode(vertices(), value.getValue(), caseSensitive), codeSystem) .repeat(__.in(FHIRTermGraph.IS_A) .simplePath() .dedup()) @@ -198,12 +200,11 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { g = whereCodeSystem(g.not(__.repeat(__.out(FHIRTermGraph.IS_A).simplePath()) .until(hasCode(value.getValue(), caseSensitive))) - .not(hasCode(value.getValue(), caseSensitive)) - .hasLabel("Concept"), codeSystem); + .not(hasCode(value.getValue(), caseSensitive)), codeSystem); } break; case NOT_IN: - if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { + if ("concept".equals(property.getValue())) { if (caseSensitive) { g = whereCodeSystem(g.has("code", P.without(Arrays.stream(value.getValue().split(",")) .collect(Collectors.toSet()))), codeSystem); @@ -212,11 +213,16 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { .map(FHIRTermGraphUtil::normalize) .collect(Collectors.toSet()))), codeSystem); } + } else { + g = whereCodeSystem(g.has(getPropertyKey(type), P.without(Arrays.stream(value.getValue().split(",")) + .map(v -> toElement(v, type)) + .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) + .collect(Collectors.toSet()))).in("property_"), codeSystem); } break; case REGEX: - if (hasCodeSystemProperty(codeSystem, property) && PropertyType.STRING.equals(getCodeSystemPropertyType(codeSystem, property))) { - g = whereCodeSystem(g.has("valueString", Text.textContainsRegex(value.getValue())), codeSystem); + if (hasCodeSystemProperty(codeSystem, property) && (PropertyType.CODE.equals(type) || PropertyType.STRING.equals(type))) { + g = whereCodeSystem(g.has(getPropertyKey(type), Text.textRegex(value.getValue())).in("property_"), codeSystem); } break; default: @@ -230,7 +236,8 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { .toStream() .forEach(elementMap -> concepts.add(createConcept(elementMap))); */ - g.elementMap().toStream().forEach(elementMap -> concepts.add(createConcept(elementMap))); + + g.hasLabel("Concept").elementMap().toStream().forEach(elementMap -> concepts.add(createConcept(elementMap))); /* if (timeLimitStep.getTimedOut()) { @@ -386,15 +393,21 @@ private Element getValue(Map elementMap) { if (elementMap.containsKey("valueDateTime")) { return DateTime.of((String) elementMap.get("valueDateTime")); } + /* if (elementMap.containsKey("valueDecimal")) { return Decimal.of((Double) elementMap.get("valueDecimal")); } + */ + if (elementMap.containsKey("valueDecimalString")) { + return Decimal.of((String) elementMap.get("valueDecimalString")); + } if (elementMap.containsKey("valueInteger")) { return com.ibm.fhir.model.type.Integer.of((Integer) elementMap.get("valueInteger")); } if (elementMap.containsKey("valueString")) { return string((String) elementMap.get("valueString")); } + System.out.println("elementMap: " + elementMap); return null; } diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java index 8ac1842335f..b006c888c48 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/CodeSystemTermGraphLoaderTest.java @@ -12,7 +12,7 @@ import java.util.Set; import org.apache.commons.configuration.PropertiesConfiguration; -import org.locationtech.jts.util.Assert; +import org.testng.Assert; import org.testng.annotations.Test; import com.ibm.fhir.model.format.Format; @@ -36,7 +36,7 @@ public void testCodeSystemTermGraphLoader() throws Exception { FHIRTermGraphUtil.setRootLoggerLevel(Level.INFO); FHIRTermGraph graph = null; - try (InputStream in = CodeSystemTermGraphLoaderTest.class.getClassLoader().getResourceAsStream("JSON/CodeSystem-cs5.json")) { + try (InputStream in = CodeSystemTermGraphLoaderTest.class.getClassLoader().getResourceAsStream("JSON/CodeSystem-test.json")) { graph = FHIRTermGraphFactory.open(new PropertiesConfiguration("conf/janusgraph-berkeleyje-lucene.properties")); graph.dropAllVertices(); @@ -46,6 +46,11 @@ public void testCodeSystemTermGraphLoader() throws Exception { FHIRTermServiceProvider provider = new GraphTermServiceProvider(graph); + Set actual = new LinkedHashSet<>(); + for (Concept concept : provider.getConcepts(codeSystem)) { + actual.add(provider.getConcept(codeSystem, concept.getCode())); + } + Set expected = new LinkedHashSet<>(); for (Concept concept : CodeSystemSupport.getConcepts(codeSystem)) { expected.add(concept.toBuilder() @@ -53,12 +58,7 @@ public void testCodeSystemTermGraphLoader() throws Exception { .build()); } - Set actual = new LinkedHashSet<>(); - for (Concept concept : provider.getConcepts(codeSystem)) { - actual.add(provider.getConcept(codeSystem, concept.getCode())); - } - - Assert.equals(expected, actual); + Assert.assertEquals(actual, expected); } finally { if (graph != null) { graph.close(); diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java index 8dd5180fedf..6e642198572 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java @@ -6,50 +6,567 @@ package com.ibm.fhir.term.graph.test; -import java.util.HashMap; -import java.util.Map; +import static com.ibm.fhir.model.type.String.string; + +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; -import org.apache.commons.configuration.MapConfiguration; +import org.apache.commons.configuration.PropertiesConfiguration; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import com.ibm.fhir.model.format.Format; +import com.ibm.fhir.model.parser.FHIRParser; import com.ibm.fhir.model.resource.CodeSystem; import com.ibm.fhir.model.resource.CodeSystem.Concept; +import com.ibm.fhir.model.resource.ValueSet.Compose.Include.Filter; import com.ibm.fhir.model.type.Code; -import com.ibm.fhir.model.type.Uri; -import com.ibm.fhir.model.type.code.CodeSystemContentMode; -import com.ibm.fhir.model.type.code.PublicationStatus; +import com.ibm.fhir.model.type.code.FilterOperator; +import com.ibm.fhir.term.graph.FHIRTermGraph; +import com.ibm.fhir.term.graph.factory.FHIRTermGraphFactory; +import com.ibm.fhir.term.graph.loader.FHIRTermGraphLoader; +import com.ibm.fhir.term.graph.loader.impl.CodeSystemTermGraphLoader; import com.ibm.fhir.term.graph.provider.GraphTermServiceProvider; +import com.ibm.fhir.term.graph.util.FHIRTermGraphUtil; +import com.ibm.fhir.term.spi.FHIRTermServiceProvider; +import com.ibm.fhir.term.util.CodeSystemSupport; + +import ch.qos.logback.classic.Level; public class GraphTermServiceProviderTest { - public static void main(String[] args) throws Exception { - CodeSystem codeSystem = CodeSystem.builder() - .url(Uri.of("http://snomed.info/sct")) - .status(PublicationStatus.ACTIVE) - .content(CodeSystemContentMode.NOT_PRESENT) + private FHIRTermGraph graph = null; + private CodeSystem codeSystem = null; + private FHIRTermServiceProvider provider = null; + + @BeforeClass + public void beforeClass() throws Exception { + FHIRTermGraphUtil.setRootLoggerLevel(Level.INFO); + + try (InputStream in = CodeSystemTermGraphLoaderTest.class.getClassLoader().getResourceAsStream("JSON/CodeSystem-test.json")) { + graph = FHIRTermGraphFactory.open(new PropertiesConfiguration("conf/janusgraph-berkeleyje-lucene.properties")); + graph.dropAllVertices(); + + codeSystem = FHIRParser.parser(Format.JSON).parse(in); + FHIRTermGraphLoader loader = new CodeSystemTermGraphLoader(graph, codeSystem); + loader.load(); + + provider = new GraphTermServiceProvider(graph); + } + } + + @AfterClass + public void afterClass() { + if (graph != null) { + graph.close(); + } + } + + @Test + public void testClosure() { + List actual = provider.closure(codeSystem, Code.of("d")).stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("d", "q", "r", "s"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConcepts() { + Set actual = new LinkedHashSet<>(); + for (Concept concept : provider.getConcepts(codeSystem)) { + actual.add(provider.getConcept(codeSystem, concept.getCode())); + } + + Set expected = new LinkedHashSet<>(); + for (Concept concept : CodeSystemSupport.getConcepts(codeSystem)) { + expected.add(concept.toBuilder() + .concept(Collections.emptyList()) + .build()); + } + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithDescendantOfFilter() { + Filter filter = Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.DESCENDENT_OF) + .value(string("d")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("q", "r", "s"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithParentEqualsFilter() { + Filter filter = Filter.builder() + .property(Code.of("parent")) + .op(FilterOperator.EQUALS) + .value(string("d")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("q"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithChildEqualsFilter() { + Filter filter = Filter.builder() + .property(Code.of("child")) + .op(FilterOperator.EQUALS) + .value(string("q")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("d"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithBooleanPropertyEqualsFilter() { + Filter filter = Filter.builder() + .property(Code.of("booleanProperty")) + .op(FilterOperator.EQUALS) + .value(string("true")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("o"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithCodePropertyEqualsFilter() { + Filter filter = Filter.builder() + .property(Code.of("codeProperty")) + .op(FilterOperator.EQUALS) + .value(string("codeValue")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("y"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithDateTimePropertyEqualsFilter() { + Filter filter = Filter.builder() + .property(Code.of("dateTimeProperty")) + .op(FilterOperator.EQUALS) + .value(string("2021-01-01T00:00:00.000Z")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("k"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithDecimalPropertyEqualsFilter() { + Filter filter = Filter.builder() + .property(Code.of("decimalProperty")) + .op(FilterOperator.EQUALS) + .value(string("-101.01")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("n"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithIntegerPropertyEqualsFilter() { + Filter filter = Filter.builder() + .property(Code.of("integerProperty")) + .op(FilterOperator.EQUALS) + .value(string("5")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("s"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithStringPropertyEqualsFilter() { + Filter filter = Filter.builder() + .property(Code.of("stringProperty")) + .op(FilterOperator.EQUALS) + .value(string("stringValue")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("a"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithPropertyExistsFilter() { + Filter filter = Filter.builder() + .property(Code.of("stringProperty")) + .op(FilterOperator.EXISTS) + .value(string("true")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("a"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithPropertyNotExistsFilter() { + Filter filter = Filter.builder() + .property(Code.of("stringProperty")) + .op(FilterOperator.EXISTS) + .value(string("false")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + Set actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toSet()); + + Set expected = new HashSet<>(Arrays.asList("b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z")); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithGeneralizesFilter() { + Filter filter = Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.GENERALIZES) + .value(string("s")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("s", "r", "q", "d"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithConceptInFilter() { + Filter filter = Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IN) + .value(string("a,b,c")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("a", "b", "c"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithBooleanPropertyInFilter() { + Filter filter = Filter.builder() + .property(Code.of("booleanProperty")) + .op(FilterOperator.IN) + .value(string("true,false")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("o"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithCodePropertyInFilter() { + Filter filter = Filter.builder() + .property(Code.of("codeProperty")) + .op(FilterOperator.IN) + .value(string("a,b,c,codeValue")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("y"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithDateTimePropertyInFilter() { + Filter filter = Filter.builder() + .property(Code.of("dateTimeProperty")) + .op(FilterOperator.IN) + .value(string("2019-01-01T00:00:00.000Z,2020-01-01T00:00:00.000Z,2021-01-01T00:00:00.000Z")) .build(); - System.out.println(codeSystem); + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); - Map map = new HashMap<>(); - map.put("storage.backend", "cql"); - map.put("storage.hostname", "127.0.0.1"); - map.put("index.search.backend", "elasticsearch"); - map.put("index.search.hostname", "127.0.0.1:9200"); - map.put("query.batch", true); - map.put("query.batch-property-prefetch", true); - map.put("query.fast-property", true); - map.put("storage.read-only", true); + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); - GraphTermServiceProvider provider = new GraphTermServiceProvider(new MapConfiguration(map)); + List expected = Arrays.asList("k"); - Set concepts = provider.closure(codeSystem, Code.of("195967001")); - concepts.stream().forEach(System.out::println); + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithDecimalPropertyInFilter() { + Filter filter = Filter.builder() + .property(Code.of("decimalProperty")) + .op(FilterOperator.IN) + .value(string("100.0,101.11,-101.11,-101.01")) + .build(); - System.out.println(provider.subsumes(codeSystem, Code.of("195967001"), Code.of("31387002"))); - System.out.println(provider.subsumes(codeSystem, Code.of("195967001"), Code.of("195967001"))); + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("n"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithIntegerPropertyInFilter() { + Filter filter = Filter.builder() + .property(Code.of("integerProperty")) + .op(FilterOperator.IN) + .value(string("1,2,3,4,5")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("s"); + + Assert.assertEquals(actual, expected); + } - System.out.println(provider.getConcept(codeSystem, Code.of("195967001"))); + @Test + public void testGetConceptsWithStringPropertyInFilter() { + Filter filter = Filter.builder() + .property(Code.of("stringProperty")) + .op(FilterOperator.IN) + .value(string("a,b,c,stringValue")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("a"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithIsAFilter() { + Filter filter = Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IS_A) + .value(string("d")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("d", "q", "r", "s"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithIsNotAFilter() { + Filter filter = Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IS_NOT_A) + .value(string("d")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + Set actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toSet()); + + Set expected = new HashSet<>(Arrays.asList("a", "b", "c", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "t", "u", "v", "w", "x", "y", "z")); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithStringPropertyNotInFilter() { + Filter filter = Filter.builder() + .property(Code.of("stringProperty")) + .op(FilterOperator.NOT_IN) + .value(string("a,b,c")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("a"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithCodePropertRegexFilter() { + Filter filter = Filter.builder() + .property(Code.of("codeProperty")) + .op(FilterOperator.REGEX) + .value(string(".*code.*")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("y"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithStringPropertyRegexFilter() { + Filter filter = Filter.builder() + .property(Code.of("stringProperty")) + .op(FilterOperator.REGEX) + .value(string(".*str.*")) + .build(); + + Set concepts = provider.getConcepts(codeSystem, Collections.singletonList(filter)); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("a"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testIsSupported() { + Assert.assertTrue(provider.isSupported(codeSystem)); + } + + @Test + public void testSubsumes() { + Assert.assertTrue(provider.subsumes(codeSystem, Code.of("d"), Code.of("s"))); + Assert.assertFalse(provider.subsumes(codeSystem, Code.of("a"), Code.of("b"))); + } - provider.getGraph().close(); + @Test + public void testHasConcept() { + Assert.assertTrue(provider.hasConcept(codeSystem, Code.of("a"))); + Assert.assertFalse(provider.hasConcept(codeSystem, Code.of("zzz"))); } } diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedGraphTermServiceProviderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedGraphTermServiceProviderTest.java new file mode 100644 index 00000000000..afea26aa865 --- /dev/null +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedGraphTermServiceProviderTest.java @@ -0,0 +1,55 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.test; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.configuration.MapConfiguration; + +import com.ibm.fhir.model.resource.CodeSystem; +import com.ibm.fhir.model.resource.CodeSystem.Concept; +import com.ibm.fhir.model.type.Code; +import com.ibm.fhir.model.type.Uri; +import com.ibm.fhir.model.type.code.CodeSystemContentMode; +import com.ibm.fhir.model.type.code.PublicationStatus; +import com.ibm.fhir.term.graph.provider.GraphTermServiceProvider; + +public class SnomedGraphTermServiceProviderTest { + public static void main(String[] args) throws Exception { + CodeSystem codeSystem = CodeSystem.builder() + .url(Uri.of("http://snomed.info/sct")) + .status(PublicationStatus.ACTIVE) + .content(CodeSystemContentMode.NOT_PRESENT) + .build(); + + System.out.println(codeSystem); + + Map map = new HashMap<>(); + map.put("storage.backend", "cql"); + map.put("storage.hostname", "127.0.0.1"); + map.put("index.search.backend", "elasticsearch"); + map.put("index.search.hostname", "127.0.0.1:9200"); + map.put("query.batch", true); + map.put("query.batch-property-prefetch", true); + map.put("query.fast-property", true); + map.put("storage.read-only", true); + + GraphTermServiceProvider provider = new GraphTermServiceProvider(new MapConfiguration(map)); + + Set concepts = provider.closure(codeSystem, Code.of("195967001")); + concepts.stream().forEach(System.out::println); + + System.out.println(provider.subsumes(codeSystem, Code.of("195967001"), Code.of("31387002"))); + System.out.println(provider.subsumes(codeSystem, Code.of("195967001"), Code.of("195967001"))); + + System.out.println(provider.getConcept(codeSystem, Code.of("195967001"))); + + provider.getGraph().close(); + } +} diff --git a/fhir-term-graph/src/test/resources/JSON/CodeSystem-cs5.json b/fhir-term-graph/src/test/resources/JSON/CodeSystem-cs5.json deleted file mode 100644 index b4bff4fccd4..00000000000 --- a/fhir-term-graph/src/test/resources/JSON/CodeSystem-cs5.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "resourceType": "CodeSystem", - "id": "cs5", - "url": "http://ibm.com/fhir/CodeSystem/cs5", - "version": "1.0.0", - "status": "active", - "hierarchyMeaning": "is-a", - "content": "complete", - "property": [ - { - "code": "label", - "uri": "http://ibm.com/fhir/concept-properties#label", - "type": "string" - }, - { - "code": "property1", - "uri": "http://ibm.com/fhir/concept-properties#property1", - "type": "string" - } - ], - "concept": [ - { - "code": "m", - "display": "concept m", - "concept": [ - { - "code": "p", - "display": "concept p", - "concept": [ - { - "code": "q", - "display": "concept q" - }, - { - "code": "r", - "display": "concept r" - } - ] - } - ] - }, - { - "code": "n", - "display": "concept n", - "concept": [ - { - "code": "s", - "display": "concept s" - } - ] - }, - { - "code": "o", - "display": "concept o", - "property": [ - { - "code": "label", - "valueString": "concept label" - } - ] - }, - { - "code": "t", - "display": "concept t", - "property": [ - { - "code": "property1", - "valueString": "value1" - } - ], - "concept": [ - { - "code": "u", - "display": "concept u" - } - ] - } - ] -} \ No newline at end of file diff --git a/fhir-term-graph/src/test/resources/JSON/CodeSystem-test.json b/fhir-term-graph/src/test/resources/JSON/CodeSystem-test.json new file mode 100644 index 00000000000..12a4887fb83 --- /dev/null +++ b/fhir-term-graph/src/test/resources/JSON/CodeSystem-test.json @@ -0,0 +1,211 @@ +{ + "resourceType": "CodeSystem", + "id": "test", + "url": "http://ibm.com/fhir/CodeSystem/test", + "version": "1.0.0", + "status": "active", + "hierarchyMeaning": "is-a", + "content": "complete", + "property": [ + { + "code": "booleanProperty", + "uri": "http://ibm.com/fhir/concept-properties#booleanProperty", + "type": "boolean" + }, + { + "code": "codeProperty", + "uri": "http://ibm.com/fhir/concept-properties#codeProperty", + "type": "code" + }, + { + "code": "dateTimeProperty", + "uri": "http://ibm.com/fhir/concept-properties#dateTimeProperty", + "type": "dateTime" + }, + { + "code": "decimalProperty", + "uri": "http://ibm.com/fhir/concept-properties#decimalProperty", + "type": "decimal" + }, + { + "code": "integerProperty", + "uri": "http://ibm.com/fhir/concept-properties#integerProperty", + "type": "integer" + }, + { + "code": "stringProperty", + "uri": "http://ibm.com/fhir/concept-properties#stringProperty", + "type": "string" + } + ], + "concept": [ + { + "code": "a", + "display": "Concept A", + "property": [ + { + "code": "stringProperty", + "valueString": "stringValue" + } + ], + "concept": [ + { + "code": "g", + "display": "Concept G" + }, + { + "code": "h", + "display": "Concept H" + }, + { + "code": "i", + "display": "Concept I", + "concept": [ + { + "code": "j", + "display": "Concept J", + "concept": [ + { + "code": "k", + "display": "Concept K", + "property": [ + { + "code": "dateTimeProperty", + "valueDateTime": "2021-01-01T00:00:00.000Z" + } + ] + } + ] + } + ] + } + ] + }, + { + "code": "b", + "display": "Concept B", + "concept": [ + { + "code": "l", + "display": "Concept L", + "concept": [ + { + "code": "m", + "display": "Concept M" + }, + { + "code": "n", + "display": "Concept N", + "property": [ + { + "code": "decimalProperty", + "valueDecimal": -101.01 + } + ] + } + ] + } + ] + }, + { + "code": "c", + "display": "Concept C", + "concept": [ + { + "code": "o", + "display": "Concept O", + "property": [ + { + "code": "booleanProperty", + "valueBoolean": true + } + ] + }, + { + "code": "p", + "display": "Concept P" + } + ] + }, + { + "code": "d", + "display": "Concept D", + "concept": [ + { + "code": "q", + "display": "Concept Q", + "concept": [ + { + "code": "r", + "display": "Concept R", + "concept": [ + { + "code": "s", + "display": "Concept S", + "property": [ + { + "code": "integerProperty", + "valueInteger": 5 + } + ] + } + ] + } + ] + } + ] + }, + { + "code": "e", + "display": "Concept E", + "concept": [ + { + "code": "t", + "display": "Concept T", + "concept": [ + { + "code": "u", + "display": "Concept U", + "concept": [ + { + "code": "v", + "display": "Concept V" + } + ] + } + ] + }, + { + "code": "w", + "display": "Concept W" + } + ] + }, + { + "code": "f", + "display": "Concept F", + "concept": [ + { + "code": "x", + "display": "Concept X" + }, + { + "code": "y", + "display": "Concept Y", + "property": [ + { + "code": "codeProperty", + "valueCode": "codeValue" + } + ], + "concept": [ + { + "code": "z", + "display": "Concept Z" + } + ] + } + ] + } + ] +} \ No newline at end of file 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 90b4377b485..b2d8b732643 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 @@ -7,6 +7,7 @@ package com.ibm.fhir.term.util; import static com.ibm.fhir.core.util.LRUCache.createLRUCache; +import static com.ibm.fhir.model.type.String.string; import java.util.ArrayList; import java.util.Arrays; @@ -333,6 +334,26 @@ 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); + case CODE: + return Code.of(value); +// case CODING: + case DATE_TIME: + return DateTime.of(value); + case DECIMAL: + return Decimal.of(value); + case INTEGER: + return Integer.of(value); + case STRING: + return string(value); + default: + return null; + } + } + private static boolean accept(List conceptFilters, Concept concept) { for (ConceptFilter conceptFilter : conceptFilters) { if (!conceptFilter.accept(concept)) { @@ -468,7 +489,8 @@ private static ConceptFilter createNotInFilter(CodeSystem codeSystem, Include.Fi private static ConceptFilter createRegexFilter(CodeSystem codeSystem, Include.Filter filter) { Code property = filter.getProperty(); - if (hasCodeSystemProperty(codeSystem, property) && PropertyType.STRING.equals(getCodeSystemPropertyType(codeSystem, property))) { + PropertyType type = getCodeSystemPropertyType(codeSystem, property); + if (hasCodeSystemProperty(codeSystem, property) && (PropertyType.CODE.equals(type) || PropertyType.STRING.equals(type))) { return new RegexFilter(property, filter.getValue()); } return null; From 1653a589a2e22fe6f3e05a0be12d7ad87e806874 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Thu, 11 Mar 2021 12:23:35 -0500 Subject: [PATCH 14/50] Issue #1980 - minor update Signed-off-by: John T.E. Timm --- .../provider/GraphTermServiceProvider.java | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) 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 4b958a529c1..44d1781b4a2 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 @@ -204,20 +204,22 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { } break; case NOT_IN: - if ("concept".equals(property.getValue())) { - if (caseSensitive) { - g = whereCodeSystem(g.has("code", P.without(Arrays.stream(value.getValue().split(",")) - .collect(Collectors.toSet()))), codeSystem); + if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { + if ("concept".equals(property.getValue())) { + if (caseSensitive) { + g = whereCodeSystem(g.has("code", P.without(Arrays.stream(value.getValue().split(",")) + .collect(Collectors.toSet()))), codeSystem); + } else { + g = whereCodeSystem(g.has("codeLowerCase", P.without(Arrays.stream(value.getValue().split(",")) + .map(FHIRTermGraphUtil::normalize) + .collect(Collectors.toSet()))), codeSystem); + } } else { - g = whereCodeSystem(g.has("codeLowerCase", P.without(Arrays.stream(value.getValue().split(",")) - .map(FHIRTermGraphUtil::normalize) - .collect(Collectors.toSet()))), codeSystem); + g = whereCodeSystem(g.has(getPropertyKey(type), P.without(Arrays.stream(value.getValue().split(",")) + .map(v -> toElement(v, type)) + .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) + .collect(Collectors.toSet()))).in("property_"), codeSystem); } - } else { - g = whereCodeSystem(g.has(getPropertyKey(type), P.without(Arrays.stream(value.getValue().split(",")) - .map(v -> toElement(v, type)) - .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) - .collect(Collectors.toSet()))).in("property_"), codeSystem); } break; case REGEX: From f1481d5d3711e836e4c45d43408be5e18965e06a Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Thu, 11 Mar 2021 12:46:30 -0500 Subject: [PATCH 15/50] Issue #1980 - more unit tests Signed-off-by: John T.E. Timm --- .../test/GraphTermServiceProviderTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java index 6e642198572..6116ac04068 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java @@ -28,6 +28,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.Filter; +import com.ibm.fhir.model.type.Boolean; import com.ibm.fhir.model.type.Code; import com.ibm.fhir.model.type.code.FilterOperator; import com.ibm.fhir.term.graph.FHIRTermGraph; @@ -80,6 +81,29 @@ public void testClosure() { Assert.assertEquals(actual, expected); } + @Test + public void testGetConcept() { + Concept expected = CodeSystemSupport.findConcept(codeSystem, Code.of("a")).toBuilder() + .concept(Collections.emptyList()) + .build(); + Assert.assertEquals(provider.getConcept(codeSystem, Code.of("a")), expected); + Assert.assertNull(provider.getConcept(codeSystem, Code.of("zzz"))); + } + + @Test + public void testGetConceptCaseSensitive() { + CodeSystem caseSensitiveCodeSystem = codeSystem.toBuilder().caseSensitive(Boolean.TRUE).build(); + Assert.assertNotNull(provider.getConcept(caseSensitiveCodeSystem, Code.of("a"))); + Assert.assertNull(provider.getConcept(caseSensitiveCodeSystem, Code.of("A"))); + } + + @Test + public void testGetConceptCaseInsensitive() { + CodeSystem caseInsensitiveCodeSystem = codeSystem.toBuilder().caseSensitive(Boolean.FALSE).build(); + Assert.assertNotNull(provider.getConcept(caseInsensitiveCodeSystem, Code.of("a"))); + Assert.assertNotNull(provider.getConcept(caseInsensitiveCodeSystem, Code.of("A"))); + } + @Test public void testGetConcepts() { Set actual = new LinkedHashSet<>(); From 50617bfc3ac8c553ecd2da3cbb8e2a79933a39e3 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Thu, 11 Mar 2021 14:57:58 -0500 Subject: [PATCH 16/50] Issue #1980 - added Javadoc Signed-off-by: John T.E. Timm --- .../ibm/fhir/term/graph/FHIRTermGraph.java | 63 ++++++++++++++++++- .../term/graph/impl/FHIRTermGraphImpl.java | 4 +- .../graph/loader/FHIRTermGraphLoader.java | 22 +++++++ .../ibm/fhir/term/util/CodeSystemSupport.java | 63 +++++++++++++++++++ 4 files changed, 148 insertions(+), 4 deletions(-) diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java index 90329e9548d..fd20d693f4d 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java @@ -18,15 +18,74 @@ * A graph that represents FHIR CodeSystem content and is backed by a graph database (Janusgraph) */ public interface FHIRTermGraph { + /** + * The edge label that represents an is-a relationship in the graph + */ public static final String IS_A = "isa"; + + /** + * Get the configuration used to create this {@link FHIRTermGraph} + * + * @return + * the configuration + */ Configuration configuration(); + + /** + * Get the underlying {@link JanusGraph} instance behind this {@link FHIRTermGraph} + * + * @return + * the {@link JanusGraph} instance + */ JanusGraph getJanusGraph(); + + /** + * Get the graph traversal source associated with the underlying {@link JanusGraph} instance + * + * @return + * the graph traversal source + */ GraphTraversalSource traversal(); + + /** + * Query the indexing backend using the Lucene query parser syntax + * + * @param query + * the query + * @return + * results of the specified query + */ default Stream> indexQuery(String query) { - return indexQuery(query, Integer.MAX_VALUE - 1); + return indexQuery(query, Integer.MAX_VALUE - 1, 0); } - Stream> indexQuery(String query, int limit); + + /** + * Query the indexing backend using the Lucene query parser syntax + * and the provided limit and offset + * + * @param query + * the query + * @param limit + * the limit + * @param offset + * the offset + * @return + * results of the specified query using the provided limit and offset + */ + Stream> indexQuery(String query, int limit, int offset); + + /** + * Close the graph and its underlying resources + */ void close(); + + /** + * Drop the graph + */ void drop(); + + /** + * Drop all vertices and edges from the graph + */ void dropAllVertices(); } diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java index 873551953a6..b7f0ccbcda6 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java @@ -163,8 +163,8 @@ public GraphTraversalSource traversal() { } @Override - public Stream> indexQuery(String query, int limit) { - return graph.indexQuery("vertices", query).limit(limit).vertexStream(); + public Stream> indexQuery(String query, int limit, int offset) { + return graph.indexQuery("vertices", query).limit(limit).offset(offset).vertexStream(); } @Override diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java index f6b1561eaab..c28fd2f0cbd 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java @@ -48,8 +48,30 @@ public Options options() { public abstract Options options(); } + + /** + * Load the {@link FHIRTermGraph} + */ void load(); + + /** + * Close the loader and its underlying resources + */ void close(); + + /** + * Get the options used to create this {@link FHIRTermGraphLoader} + * + * @return + * the options + */ Map options(); + + /** + * Get the underlying {@link FHIRTermGraph} instance + * + * @return + * the {@link FHIRTermGraph} instance + */ FHIRTermGraph getGraph(); } 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 b2d8b732643..fedc9dab5af 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 @@ -46,6 +46,14 @@ public final class CodeSystemSupport { private CodeSystemSupport() { } + /** + * Determine if the given FHIR string value can be converted to a FHIR Boolean value. + * + * @param value + * the FHIR string value + * @return + * true if the given FHIR string value can be converted to a FHIR Boolean value, false otherwise + */ public static boolean convertsToBoolean(String value) { return "true".equals(value.getValue()) || "false".equals(value.getValue()); } @@ -97,6 +105,19 @@ 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; } @@ -170,6 +191,18 @@ public static CodeSystem getCodeSystem(java.lang.String url) { return FHIRRegistry.getInstance().getResource(url, CodeSystem.class); } + /** + * Get the code system filter with the given property code and filter operator. + * + * @param codeSystem + * the code system + * @param code + * the property code + * @param operator + * the filter operator + * @return + * the code system filter with the given property code and filter operator, or null if no such filter exists + */ public static CodeSystem.Filter getCodeSystemFilter(CodeSystem codeSystem, Code code, FilterOperator operator) { for (CodeSystem.Filter filter : codeSystem.getFilter()) { if (filter.getCode().equals(code) && filter.getOperator().contains(operator)) { @@ -310,10 +343,29 @@ public static Set getConcepts(Concept concept) { return concepts; } + /** + * Convert the given FHIR string value to a FHIR boolean value. + * + * @param value + * the FHIR string value + * @return + * the FHIR boolean value equivalent of the provided FHIR string value + */ public static Boolean toBoolean(String value) { return "true".equals(value.getValue()) ? Boolean.TRUE : Boolean.FALSE; } + /** + * Convert the given FHIR string value to an Element value based on the provided property type. + * + * @param value + * the FHIR string value + * @param type + * the property type + * @return + * 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(String value, PropertyType type) { switch (type.getValueAsEnumConstant()) { case BOOLEAN: @@ -334,6 +386,17 @@ public static Element toElement(String value, PropertyType type) { } } + /** + * Convert the given Java string value to an Element based on the provided property type. + * + * @param value + * the Java string value + * @param type + * the property type + * @return + * 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(java.lang.String value, PropertyType type) { switch (type.getValueAsEnumConstant()) { case BOOLEAN: From bc3e38fedbbb7432be0b3ec958152f300d80b7f0 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Thu, 11 Mar 2021 15:58:38 -0500 Subject: [PATCH 17/50] Issue #1980 - update docs Signed-off-by: John T.E. Timm --- docs/src/pages/guides/FHIRServerUsersGuide.md | 6 ++ docs/src/pages/guides/FHIRTerminologyGuide.md | 82 +++++++++++-------- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/docs/src/pages/guides/FHIRServerUsersGuide.md b/docs/src/pages/guides/FHIRServerUsersGuide.md index 35e5f24b31f..21d7d832e7f 100644 --- a/docs/src/pages/guides/FHIRServerUsersGuide.md +++ b/docs/src/pages/guides/FHIRServerUsersGuide.md @@ -1946,6 +1946,8 @@ This section contains reference information about each of the configuration prop |`fhirServer/core/allowClientHandlingPref`|boolean|Indicates whether the client is allowed to override the server default handling preference using the `Prefer:handling` header value part.| |`fhirServer/core/checkReferenceTypes`|boolean|Indicates whether reference type checking is performed by the server during parsing / deserialization.| |`fhirServer/core/serverRegistryResourceProviderEnabled`|boolean|Indicates whether the server registry resource provider should be used by the FHIR registry component to access definitional resources through the persistence layer.| +|`fhirServer/core/graphTermServiceProvider/enabled`|boolean|Indicates whether the graph term service provider should be used by the FHIR term service to access code system content| +|`fhirServer/core/graphTermServiceProvider/configuration`|object (name/value pairs)|A JSON object that contains the name/value pairs used to configure the graph database used by the graph term service provider see: https://docs.janusgraph.org/basics/configuration-reference/| |`fhirServer/core/conditionalDeleteMaxNumber`|integer|The max number of matches supported in conditional delete. | |`fhirServer/core/capabilityStatementCacheTimeout`|integer|The number of minutes that a tenant's CapabilityStatement is cached for the metadata endpoint. | |`fhirServer/core/extendedCodeableConceptValidation`|boolean|A boolean flag which indicates whether extended validation is performed by the server during object construction for code, Coding, CodeableConcept, Quantity, Uri, and String elements which have required bindings to value sets.| @@ -2051,6 +2053,8 @@ This section contains reference information about each of the configuration prop |`fhirServer/core/allowClientHandlingPref`|true| |`fhirServer/core/checkReferenceTypes`|true| |`fhirServer/core/serverRegistryResourceProviderEnabled`|false| +|`fhirServer/core/graphTermServiceProvider/enabled`|false| +|`fhirServer/core/graphTermServiceProvider/configuration`|null| |`fhirServer/core/conditionalDeleteMaxNumber`|10| |`fhirServer/core/capabilityStatementCacheTimeout`|60| |`fhirServer/core/extendedCodeableConceptValidation`|true| @@ -2138,6 +2142,8 @@ must restart the server for that change to take effect. |`fhirServer/core/allowClientHandlingPref`|Y|Y| |`fhirServer/core/checkReferenceTypes`|N|N| |`fhirServer/core/serverRegistryResourceProviderEnabled`|N|N| +|`fhirServer/core/graphTermServiceProvider/enabled`|N|N| +|`fhirServer/core/graphTermServiceProvider/configuration`|N|N| |`fhirServer/core/conditionalDeleteMaxNumber`|Y|Y| |`fhirServer/core/capabilityStatementCacheTimeout`|Y|Y| |`fhirServer/core/extendedCodeableConceptValidation`|N|N| diff --git a/docs/src/pages/guides/FHIRTerminologyGuide.md b/docs/src/pages/guides/FHIRTerminologyGuide.md index 4d3b59ef927..b76cd5334f3 100644 --- a/docs/src/pages/guides/FHIRTerminologyGuide.md +++ b/docs/src/pages/guides/FHIRTerminologyGuide.md @@ -1,7 +1,7 @@ --- slug: "/FHIR/guides/FHIRTerminologyGuide/" title: "FHIR Terminology Guide" -date: "2020-06-04 12:00:00 -0400" +date: "2021-03-11 12:00:00 -0400" --- ## Overview @@ -10,23 +10,37 @@ The IBM FHIR Server Terminology module ([fhir-term](https://github.com/IBM/FHIR/ ## FHIR Terminology Service Provider Interface (SPI) -The FHIR Terminology Service Provider interface provides a mechanism for implementers to provide terminology capabilities via the Java ServiceLoader. The interface includes method signatures for `expand`, `lookup`, `subsumes`, `closure`, `validateCode` (CodeSystem) and `validateCode` (ValueSet). Here is an excerpt (for brevity) of the SPI: +The FHIR Terminology Service Provider interface provides a mechanism for implementers to provide terminology capabilities via the Java ServiceLoader. The interface includes method signatures for `closure`, `getConcept`, `getConcepts`, `hasConcept`, `isSupported` and `subsumes`: ```java public interface FHIRTermServiceProvider { - boolean isExpandable(ValueSet valueSet); - ValueSet expand(ValueSet valueSet, ExpansionParameters parameters); - LookupOutcome lookup(Coding coding, LookupParameters parameters); - ConceptSubsumptionOutcome subsumes(Coding codingA, Coding codingB); - Set closure(Coding coding); - ValidationOutcome validateCode(CodeSystem codeSystem, Coding coding, ValidationParameters parameters); - ValidationOutcome validateCode(CodeSystem codeSystem, CodeableConcept codeableConcept, ValidationParameters parameters); - ValidationOutcome validateCode(ValueSet valueSet, Coding coding, ValidationParameters parameters); - ValidationOutcome validateCode(ValueSet valueSet, CodeableConcept codeableConcept, ValidationParameters parameters); - TranslationOutcome translate(ConceptMap conceptMap, Coding coding, TranslationParameters parameters); - TranslationOutcome translate(ConceptMap conceptMap, CodeableConcept codeableConcept, TranslationParameters parameters); + Set closure(CodeSystem codeSystem, Code code); + Concept getConcept(CodeSystem codeSystem, Code code); + Set getConcepts(CodeSystem codeSystem); + Set getConcepts(CodeSystem codeSystem, List filters); + boolean hasConcept(CodeSystem codeSystem, Code code); + boolean isSupported(CodeSystem codeSystem); + boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB); } +``` + +## Default Terminology Service Provider Implementation + +The default implementation of `FHIRTermServiceProvider` ([DefaultTermServiceProvider](https://github.com/IBM/FHIR/blob/main/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java)) leverages terminology resources (`CodeSystem`, `ValueSet`, and `ConceptMap`) that have been made available through the FHIR registry module ([fhir-registry](https://github.com/IBM/FHIR/tree/main/fhir-registry)). It supports `CodeSystem` resources with *complete* content (`CodeSystem.content = 'complete'`) and `ValueSet` resources that reference `CodeSystem` resources that have complete content. + +## FHIR Terminology Service Singleton facade + +The FHIR Terminology Service Singleton facade ([FHIRTermService](https://github.com/IBM/FHIR/blob/main/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java)) loads a list of `FHIRTermServiceProvider` instances from the ServiceLoader and includes an instance of the `DefaultTermServiceProvider`. Client code (Java) that requires terminology capabilities should access them via the `FHIRTermService` singleton facade. Here is an example: +```java +ValueSet valueSet = ValueSetSupport.getValueSet("http://ibm.com/fhir/ValueSet/vs1"); +Coding coding = Coding.builder() + .system(Uri.of("http://ibm.com/fhir/CodeSystem/cs1")) + .version(string("1.0.0")) + .code(Code.of("a") + .display(string("concept a") + .build(); +ValidationOutcome outcome = FHIRTermService.getInstance().validateCode(valueSet, coding); ``` The `expand `, `lookup`, `validateCode` (CodeSystem), `validateCode` (ValueSet), and `translate` methods support the passing of optional parameters (e.g. `ExpansionParameters`, `LookupParameters`, etc.). Many of the methods also return an "outcome" object. These "parameter" and "outcome" objects are modeled after the input/output parameters specified in the terminology operations from the FHIR Terminology module: [http://hl7.org/fhir/terminology-module.html](http://hl7.org/fhir/terminology-module.html). @@ -47,24 +61,7 @@ Parameters parameters = outcome.toParameters(); This bridge to/from the `Parameters` resource enables implementers to build both native implementations of the SPI and implementations that access an existing external terminology service. -## Default Terminology Service Provider Implementation - -The default implementation of `FHIRTermServiceProvider` ([DefaultTermServiceProvider](https://github.com/IBM/FHIR/blob/main/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java)) leverages terminology resources (`CodeSystem`, `ValueSet`, and `ConceptMap`) that have been made available through the FHIR registry module ([fhir-registry](https://github.com/IBM/FHIR/tree/main/fhir-registry)). It supports `CodeSystem` resources with *complete* content (`CodeSystem.content = 'complete'`) and `ValueSet` resources that reference `CodeSystem` resources that have complete content. The default implementation does not support for optional parameters (e.g. `ExpansionParameters`, `TranslationParameters`, `ValidationParameters`, etc.). - -## FHIR Terminology Service Singleton facade - -The FHIR Terminology Service Singleton facade ([FHIRTermService](https://github.com/IBM/FHIR/blob/main/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java)) loads a `FHIRTermServiceProvider` from the ServiceLoader, if one exists. Otherwise, it will instantiate a `DefaultTermServiceProvider`. Other FHIR server components and user code (Java) that requires terminology capabilities should access them via the `FHIRTermService` singleton facade. Here is an example: - -```java -ValueSet valueSet = ValueSetSupport.getValueSet("http://ibm.com/fhir/ValueSet/vs1"); -Coding coding = Coding.builder() - .system(Uri.of("http://ibm.com/fhir/CodeSystem/cs1")) - .version(string("1.0.0")) - .code(Code.of("a") - .display(string("concept a") - .build(); -ValidationOutcome outcome = FHIRTermService.getInstance().validateCode(valueSet, coding); -``` +NOTE: The current implementation does not support for optional parameters (e.g. `ExpansionParameters`, `TranslationParameters`, `ValidationParameters`, etc.). ## FHIR Server Terminology Extended Operations @@ -98,4 +95,25 @@ Collection result = evaluator.evaluate("%terminologies.validateCod ``` -Additionally, the FHIRPath functions `subsumedBy` and `subsumes` have been implemented per: [http://hl7.org/fhir/fhirpath.html#functions](http://hl7.org/fhir/fhirpath.html#functions) \ No newline at end of file +Additionally, the FHIRPath functions `subsumedBy` and `subsumes` have been implemented per: [http://hl7.org/fhir/fhirpath.html#functions](http://hl7.org/fhir/fhirpath.html#functions) + +## Graph Terminology Service Provider Implementation (experimental) + +The FHIR term graph module ([fhir-term-graph](https://github.com/IBM/FHIR/tree/main/fhir-term-graph)) provides an implementation of `FHIRTermServiceProvider` that is backed by a graph database ([JanusGraph](https://janusgraph.org)). The module also contains term graph loaders for SNOMED-CT Release Format 2 (RF2) files, UMLS Rich Release Format (RRF) files, and FHIR CodeSystem resources. The GraphTermServiceProvider can be enabled through the `fhir-server-config.json` file per the configuration properties specified in the [FHIR Server User's Guide](https://ibm.github.io/FHIR/guides/FHIRServerUsersGuide#51-configuration-properties-reference). Example configuration: + +``` + "graphTermServiceProvider": { + "enabled": true, + "configuration": { + "storage.backend": "cql", + "storage.hostname": "127.0.0.1", + "index.search.backend": "elasticsearch", + "index.search.hostname": "127.0.0.1:9200", + "storage.read-only": true, + "query.batch": true, + "query.batch-property-prefetch": true, + "query.fast-property": true + } + } +``` + From a194450d8d51c76c1466721fad3ec28ed0da7caf Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Thu, 11 Mar 2021 16:16:27 -0500 Subject: [PATCH 18/50] Issue #1980 - more doc updates Signed-off-by: John T.E. Timm --- docs/src/pages/guides/FHIRServerUsersGuide.md | 2 +- docs/src/pages/guides/FHIRTerminologyGuide.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/pages/guides/FHIRServerUsersGuide.md b/docs/src/pages/guides/FHIRServerUsersGuide.md index 21d7d832e7f..81e11826e35 100644 --- a/docs/src/pages/guides/FHIRServerUsersGuide.md +++ b/docs/src/pages/guides/FHIRServerUsersGuide.md @@ -1947,7 +1947,7 @@ This section contains reference information about each of the configuration prop |`fhirServer/core/checkReferenceTypes`|boolean|Indicates whether reference type checking is performed by the server during parsing / deserialization.| |`fhirServer/core/serverRegistryResourceProviderEnabled`|boolean|Indicates whether the server registry resource provider should be used by the FHIR registry component to access definitional resources through the persistence layer.| |`fhirServer/core/graphTermServiceProvider/enabled`|boolean|Indicates whether the graph term service provider should be used by the FHIR term service to access code system content| -|`fhirServer/core/graphTermServiceProvider/configuration`|object (name/value pairs)|A JSON object that contains the name/value pairs used to configure the graph database used by the graph term service provider see: https://docs.janusgraph.org/basics/configuration-reference/| +|`fhirServer/core/graphTermServiceProvider/configuration`|object (name/value pairs)|A JSON object that contains the name/value pairs used to configure the graph database behind the graph term service provider see: https://docs.janusgraph.org/basics/configuration-reference/| |`fhirServer/core/conditionalDeleteMaxNumber`|integer|The max number of matches supported in conditional delete. | |`fhirServer/core/capabilityStatementCacheTimeout`|integer|The number of minutes that a tenant's CapabilityStatement is cached for the metadata endpoint. | |`fhirServer/core/extendedCodeableConceptValidation`|boolean|A boolean flag which indicates whether extended validation is performed by the server during object construction for code, Coding, CodeableConcept, Quantity, Uri, and String elements which have required bindings to value sets.| diff --git a/docs/src/pages/guides/FHIRTerminologyGuide.md b/docs/src/pages/guides/FHIRTerminologyGuide.md index b76cd5334f3..2763de6f6e4 100644 --- a/docs/src/pages/guides/FHIRTerminologyGuide.md +++ b/docs/src/pages/guides/FHIRTerminologyGuide.md @@ -99,7 +99,7 @@ Additionally, the FHIRPath functions `subsumedBy` and `subsumes` have been imple ## Graph Terminology Service Provider Implementation (experimental) -The FHIR term graph module ([fhir-term-graph](https://github.com/IBM/FHIR/tree/main/fhir-term-graph)) provides an implementation of `FHIRTermServiceProvider` that is backed by a graph database ([JanusGraph](https://janusgraph.org)). The module also contains term graph loaders for SNOMED-CT Release Format 2 (RF2) files, UMLS Rich Release Format (RRF) files, and FHIR CodeSystem resources. The GraphTermServiceProvider can be enabled through the `fhir-server-config.json` file per the configuration properties specified in the [FHIR Server User's Guide](https://ibm.github.io/FHIR/guides/FHIRServerUsersGuide#51-configuration-properties-reference). Example configuration: +The FHIR term graph module ([fhir-term-graph](https://github.com/IBM/FHIR/tree/main/fhir-term-graph)) provides an implementation of `FHIRTermServiceProvider` that is backed by a graph database ([JanusGraph](https://janusgraph.org)). The module also contains term graph loaders for SNOMED-CT Release Format 2 (RF2) files (SnomedTermGraphLoader), UMLS Rich Release Format (RRF) files (UMLSTermGraphLoader), and FHIR CodeSystem resources (CodeSystemTermGraphLoader). The GraphTermServiceProvider can be enabled through the `fhir-server-config.json` file per the configuration properties specified in the [FHIR Server User's Guide](https://ibm.github.io/FHIR/guides/FHIRServerUsersGuide#51-configuration-properties-reference). Example configuration: ``` "graphTermServiceProvider": { From c8327826665b557d416917b6640c0ce5da260dcf Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Fri, 12 Mar 2021 07:16:46 -0500 Subject: [PATCH 19/50] Apply suggestions from code review Co-authored-by: Paul Bastide Signed-off-by: John T.E. Timm --- docs/src/pages/guides/FHIRServerUsersGuide.md | 2 +- docs/src/pages/guides/FHIRTerminologyGuide.md | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/src/pages/guides/FHIRServerUsersGuide.md b/docs/src/pages/guides/FHIRServerUsersGuide.md index 81e11826e35..a14636be9c5 100644 --- a/docs/src/pages/guides/FHIRServerUsersGuide.md +++ b/docs/src/pages/guides/FHIRServerUsersGuide.md @@ -1947,7 +1947,7 @@ This section contains reference information about each of the configuration prop |`fhirServer/core/checkReferenceTypes`|boolean|Indicates whether reference type checking is performed by the server during parsing / deserialization.| |`fhirServer/core/serverRegistryResourceProviderEnabled`|boolean|Indicates whether the server registry resource provider should be used by the FHIR registry component to access definitional resources through the persistence layer.| |`fhirServer/core/graphTermServiceProvider/enabled`|boolean|Indicates whether the graph term service provider should be used by the FHIR term service to access code system content| -|`fhirServer/core/graphTermServiceProvider/configuration`|object (name/value pairs)|A JSON object that contains the name/value pairs used to configure the graph database behind the graph term service provider see: https://docs.janusgraph.org/basics/configuration-reference/| +|`fhirServer/core/graphTermServiceProvider/configuration`|object (name/value pairs)|A JSON object that contains the name/value pairs used to configure the graph database behind the graph term service provider see: [https://docs.janusgraph.org/basics/configuration-reference/](https://docs.janusgraph.org/basics/configuration-reference/)| |`fhirServer/core/conditionalDeleteMaxNumber`|integer|The max number of matches supported in conditional delete. | |`fhirServer/core/capabilityStatementCacheTimeout`|integer|The number of minutes that a tenant's CapabilityStatement is cached for the metadata endpoint. | |`fhirServer/core/extendedCodeableConceptValidation`|boolean|A boolean flag which indicates whether extended validation is performed by the server during object construction for code, Coding, CodeableConcept, Quantity, Uri, and String elements which have required bindings to value sets.| diff --git a/docs/src/pages/guides/FHIRTerminologyGuide.md b/docs/src/pages/guides/FHIRTerminologyGuide.md index 2763de6f6e4..3c1117e51b9 100644 --- a/docs/src/pages/guides/FHIRTerminologyGuide.md +++ b/docs/src/pages/guides/FHIRTerminologyGuide.md @@ -1,7 +1,7 @@ --- slug: "/FHIR/guides/FHIRTerminologyGuide/" title: "FHIR Terminology Guide" -date: "2021-03-11 12:00:00 -0400" +date: "2021-03-11" --- ## Overview @@ -99,9 +99,9 @@ Additionally, the FHIRPath functions `subsumedBy` and `subsumes` have been imple ## Graph Terminology Service Provider Implementation (experimental) -The FHIR term graph module ([fhir-term-graph](https://github.com/IBM/FHIR/tree/main/fhir-term-graph)) provides an implementation of `FHIRTermServiceProvider` that is backed by a graph database ([JanusGraph](https://janusgraph.org)). The module also contains term graph loaders for SNOMED-CT Release Format 2 (RF2) files (SnomedTermGraphLoader), UMLS Rich Release Format (RRF) files (UMLSTermGraphLoader), and FHIR CodeSystem resources (CodeSystemTermGraphLoader). The GraphTermServiceProvider can be enabled through the `fhir-server-config.json` file per the configuration properties specified in the [FHIR Server User's Guide](https://ibm.github.io/FHIR/guides/FHIRServerUsersGuide#51-configuration-properties-reference). Example configuration: +The FHIR term graph module [fhir-term-graph](https://github.com/IBM/FHIR/tree/main/fhir-term-graph) provides an implementation of `FHIRTermServiceProvider` that is backed by a graph database ([JanusGraph](https://janusgraph.org)). The module also contains term graph loaders for SNOMED-CT Release Format 2 (RF2) files (SnomedTermGraphLoader), UMLS Rich Release Format (RRF) files (UMLSTermGraphLoader), and FHIR CodeSystem resources (CodeSystemTermGraphLoader). The GraphTermServiceProvider can be enabled through the `fhir-server-config.json` file per the configuration properties specified in the [FHIR Server User's Guide](https://ibm.github.io/FHIR/guides/FHIRServerUsersGuide#51-configuration-properties-reference). Example configuration: -``` +``` json "graphTermServiceProvider": { "enabled": true, "configuration": { @@ -116,4 +116,3 @@ The FHIR term graph module ([fhir-term-graph](https://github.com/IBM/FHIR/tree/m } } ``` - From 4108a189e6f1914b3318ed4346e55b823555b5d3 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Fri, 12 Mar 2021 07:50:51 -0500 Subject: [PATCH 20/50] Issue #1980 - changes per PR review Signed-off-by: John T.E. Timm --- docs/src/pages/guides/FHIRServerUsersGuide.md | 1 - fhir-parent/pom.xml | 30 +++++++++++++++++++ fhir-term-graph/pom.xml | 6 ---- .../ibm/fhir/term/graph/FHIRTermGraph.java | 2 +- .../graph/factory/FHIRTermGraphFactory.java | 21 ++++++++++++- .../factory/FHIRTermGraphLoaderFactory.java | 13 ++++++++ .../impl/CodeSystemTermGraphLoader.java | 1 - .../loader/impl/SnomedTermGraphLoader.java | 1 - .../loader/impl/UMLSTermGraphLoader.java | 27 +---------------- .../loader/main/FHIRTermGraphLoaderMain.java | 1 - .../term/graph/util/FHIRTermGraphUtil.java | 30 +++++++++++++++++++ 11 files changed, 95 insertions(+), 38 deletions(-) diff --git a/docs/src/pages/guides/FHIRServerUsersGuide.md b/docs/src/pages/guides/FHIRServerUsersGuide.md index a14636be9c5..fb23835d040 100644 --- a/docs/src/pages/guides/FHIRServerUsersGuide.md +++ b/docs/src/pages/guides/FHIRServerUsersGuide.md @@ -2054,7 +2054,6 @@ This section contains reference information about each of the configuration prop |`fhirServer/core/checkReferenceTypes`|true| |`fhirServer/core/serverRegistryResourceProviderEnabled`|false| |`fhirServer/core/graphTermServiceProvider/enabled`|false| -|`fhirServer/core/graphTermServiceProvider/configuration`|null| |`fhirServer/core/conditionalDeleteMaxNumber`|10| |`fhirServer/core/capabilityStatementCacheTimeout`|60| |`fhirServer/core/extendedCodeableConceptValidation`|true| diff --git a/fhir-parent/pom.xml b/fhir-parent/pom.xml index ceb12f7cd44..203b2453dfd 100644 --- a/fhir-parent/pom.xml +++ b/fhir-parent/pom.xml @@ -438,6 +438,36 @@ commons-text 1.9 + + org.janusgraph + janusgraph-core + 0.5.3 + + + org.janusgraph + janusgraph-berkeleyje + 0.5.3 + + + org.janusgraph + janusgraph-cql + 0.5.3 + + + org.janusgraph + janusgraph-lucene + 0.5.3 + + + org.janusgraph + janusgraph-es + 0.5.3 + + + org.apache.tinkerpop + gremlin-driver + 3.4.6 + diff --git a/fhir-term-graph/pom.xml b/fhir-term-graph/pom.xml index 9888f10be2c..55bba7ae53b 100644 --- a/fhir-term-graph/pom.xml +++ b/fhir-term-graph/pom.xml @@ -18,32 +18,26 @@ org.janusgraph janusgraph-core - 0.5.3 org.janusgraph janusgraph-berkeleyje - 0.5.3 org.janusgraph janusgraph-cql - 0.5.3 org.janusgraph janusgraph-lucene - 0.5.3 org.janusgraph janusgraph-es - 0.5.3 org.apache.tinkerpop gremlin-driver - 3.4.6 org.slf4j diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java index fd20d693f4d..589b5f3a682 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/factory/FHIRTermGraphFactory.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/factory/FHIRTermGraphFactory.java index 915739a98c2..0e6d90809d6 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/factory/FHIRTermGraphFactory.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/factory/FHIRTermGraphFactory.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */ @@ -12,9 +12,20 @@ import com.ibm.fhir.term.graph.FHIRTermGraph; import com.ibm.fhir.term.graph.impl.FHIRTermGraphImpl; +/* + * Factory class used to create FHIRTermGraph instances + */ public final class FHIRTermGraphFactory { private FHIRTermGraphFactory() { } + /** + * Create a {@link FHIRTermGraph} instance using the given configuration properties file + * + * @param propFileName + * the configuration properties file + * @return + * the {@link FHIRTermGraph} instance + */ public static FHIRTermGraph open(String propFileName) { try { return open(new PropertiesConfiguration(propFileName)); @@ -23,6 +34,14 @@ public static FHIRTermGraph open(String propFileName) { } } + /** + * Create a {@link FHIRTermGraph} instance using the given configuration object + * + * @param configuration + * the configuration object + * @return + * the {@link FHIRTermGraph} instance + */ public static FHIRTermGraph open(Configuration configuration) { return new FHIRTermGraphImpl(configuration); } diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/factory/FHIRTermGraphLoaderFactory.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/factory/FHIRTermGraphLoaderFactory.java index dcbd3039de0..edb432ae587 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/factory/FHIRTermGraphLoaderFactory.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/factory/FHIRTermGraphLoaderFactory.java @@ -14,7 +14,20 @@ import com.ibm.fhir.term.graph.loader.impl.SnomedTermGraphLoader; import com.ibm.fhir.term.graph.loader.impl.UMLSTermGraphLoader; +/* + * Factory class used to create FHIRTermGraphLoader instances + */ public class FHIRTermGraphLoaderFactory { + /** + * Create {@link FHIRTermGraphLoader} instance using the provided type and options map + * + * @param type + * the type + * @param options + * the options map + * @return + * a {@link FHIRTermGraphLoader} instance + */ public static FHIRTermGraphLoader create(Type type, Map options) { switch (type) { case CODESYSTEM: diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java index 61f2c5bc3e0..f0ed7d2de00 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java @@ -214,7 +214,6 @@ public static void main(String[] args) throws Exception { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp("CodeSystemTermGraphLoader", options); } catch (Exception e) { - e.printStackTrace(); System.out.println("An error occurred: " + e.getMessage()); } finally { if (loader != null) { diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/SnomedTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/SnomedTermGraphLoader.java index 278a4d30d0d..8953f0e7012 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/SnomedTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/SnomedTermGraphLoader.java @@ -308,7 +308,6 @@ public static void main(String[] args) throws Exception { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp("SnomedTermGraphLoader", options); } catch (Exception e) { - e.printStackTrace(); System.out.println("An error occurred: " + e.getMessage()); } finally { if (loader != null) { diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/UMLSTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/UMLSTermGraphLoader.java index 20d00c95491..e67e385e58c 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/UMLSTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/UMLSTermGraphLoader.java @@ -17,11 +17,9 @@ import java.io.InputStreamReader; import java.io.Reader; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; @@ -58,9 +56,6 @@ public class UMLSTermGraphLoader extends AbstractTermGraphLoader { // Map of code system name to preferred label, configured in properties file private Properties codeSystemMap = new Properties(); - // Set of code systems which are case sensitive, configured in properties file - private Set caseSensitiveCodeSystems = new HashSet<>(); - // Map of code system id to corresponding vertex private Map codeSystemVertices = new ConcurrentHashMap<>(); @@ -104,7 +99,6 @@ public UMLSTermGraphLoader(Map options) { public void load() { try { loadSourceAttributes(); - loadCaseSensitiveCodeSystems(); loadConcepts(); loadRelations(); } catch (Exception e) { @@ -121,12 +115,7 @@ public void load() { private final Vertex createCodeSystemVertex(String sab) { String version = sabToVersion.get(sab); String url = (String) codeSystemMap.getOrDefault(sab, sab); - - boolean caseSensitive = false; - if (caseSensitiveCodeSystems.contains(sab)) { - caseSensitive = true; - } - Vertex csv = g.addV("CodeSystem").property("url", url).property("version", version).property("caseSensitive", caseSensitive).next(); + Vertex csv = g.addV("CodeSystem").property("url", url).property("version", version).next(); g.tx().commit(); return csv; @@ -307,20 +296,6 @@ private void loadSourceAttributes() throws IOException { } } - /** - * Loads configuration of code systems noted to be case sensitive - * - * @throws IOException - */ - private void loadCaseSensitiveCodeSystems() throws IOException { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getClassLoader().getResourceAsStream("conf/umlsSourceCaseSensitivity.txt")))) { - String line = reader.readLine().trim(); - if (!line.isEmpty()) { - caseSensitiveCodeSystems.add(reader.readLine()); - } - } - } - /** * Load UMLS data using properties provided in arguments * diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/main/FHIRTermGraphLoaderMain.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/main/FHIRTermGraphLoaderMain.java index 9696e7a04e7..790fe002855 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/main/FHIRTermGraphLoaderMain.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/main/FHIRTermGraphLoaderMain.java @@ -47,7 +47,6 @@ public static void main(String[] args) throws Exception { HelpFormatter formatter = new HelpFormatter(); formatter.printHelp(String.format("FHIRTermGraphLoaderMain %s ", type.toString()), options); } catch (Exception e) { - e.printStackTrace(); System.out.println("An error occurred: " + e.getMessage()); } finally { if (loader != null) { diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java index edfc7a1858f..1a413c99ed2 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java @@ -32,6 +32,14 @@ public class FHIRTermGraphUtil { private FHIRTermGraphUtil() { } + /** + * Convert the given element value to an object value that is compatible with the graph schema + * + * @param value + * the element value + * @return + * an object value that is compatible with the graph schema + */ public static Object toObject(Element value) { if (value.is(FHIR_BOOLEAN)) { return value.as(FHIR_BOOLEAN).getValue(); @@ -54,6 +62,14 @@ public static Object toObject(Element value) { throw new IllegalArgumentException(); } + /** + * Normalize the string by making it case and accent insensitive + * + * @param value + * the string value to normalized + * @return + * the normalized string value + */ public static String normalize(String value) { if (value != null) { return Normalizer.normalize(value, Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "").toLowerCase(); @@ -61,11 +77,25 @@ public static String normalize(String value) { return null; } + /** + * Sets the root logger level for the logback classic root logger + * + * @param level + * the level + */ public static void setRootLoggerLevel(Level level) { Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); rootLogger.setLevel(level); } + /** + * Convert the {@link DateTime} value to a Long value that is compatible with the graph schema + * + * @param dateTime + * the dateTime value + * @return + * the Long equivalent value (milliseconds from the epoch) + */ public static Long toLong(DateTime dateTime) { TemporalAccessor value = dateTime.getValue(); if (value instanceof ZonedDateTime) { From 5553872adb098292a5c3d2fcf88bab20ed7fe770 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Fri, 12 Mar 2021 08:00:35 -0500 Subject: [PATCH 21/50] Issue #1980 - removed commented out lines of code Signed-off-by: John T.E. Timm --- .../term/graph/impl/FHIRTermGraphImpl.java | 2 -- .../provider/GraphTermServiceProvider.java | 19 ------------------- .../term/graph/test/FHIRTermGraphTest.java | 2 -- 3 files changed, 23 deletions(-) diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java index b7f0ccbcda6..a98d006dc73 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/impl/FHIRTermGraphImpl.java @@ -82,7 +82,6 @@ private void createSchema(JanusGraph graph) { PropertyKey valueBoolean = management.makePropertyKey("valueBoolean").dataType(Boolean.class).make(); PropertyKey valueCode = management.makePropertyKey("valueCode").dataType(String.class).make(); -// PropertyKey valueDateTime = management.makePropertyKey("valueDateTime").dataType(String.class).make(); PropertyKey valueDateTimeLong = management.makePropertyKey("valueDateTimeLong").dataType(Long.class).make(); PropertyKey valueDecimal = management.makePropertyKey("valueDecimal").dataType(Double.class).make(); PropertyKey valueInteger = management.makePropertyKey("valueInteger").dataType(Integer.class).make(); @@ -119,7 +118,6 @@ private void createSchema(JanusGraph graph) { management.buildIndex("byValueBoolean", Vertex.class).addKey(valueBoolean).buildCompositeIndex(); management.buildIndex("byValueCode", Vertex.class).addKey(valueCode).buildCompositeIndex(); -// management.buildIndex("byValueDateTime", Vertex.class).addKey(valueDateTime).buildCompositeIndex(); management.buildIndex("byValueDateTimeLong", Vertex.class).addKey(valueDateTimeLong).buildCompositeIndex(); management.buildIndex("byValueDecimal", Vertex.class).addKey(valueDecimal).buildCompositeIndex(); management.buildIndex("byValueInteger", Vertex.class).addKey(valueInteger).buildCompositeIndex(); 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 44d1781b4a2..545a62864c2 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 @@ -108,12 +108,6 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { GraphTraversal g = vertices(); - /* - // TODO: make the time limit configurable - GraphTraversal g = vertices().timeLimit(30000L); - TimeLimitStep timeLimitStep = (TimeLimitStep) g.asAdmin().getEndStep(); - */ - boolean caseSensitive = isCaseSensitive(codeSystem); for (Filter filter : filters) { @@ -232,21 +226,8 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { } } - /* - whereCodeSystem(g, codeSystem) - .elementMap() - .toStream() - .forEach(elementMap -> concepts.add(createConcept(elementMap))); - */ - g.hasLabel("Concept").elementMap().toStream().forEach(elementMap -> concepts.add(createConcept(elementMap))); - /* - if (timeLimitStep.getTimedOut()) { - // TODO: throw new timed out exception - } - */ - return concepts; } diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java index 25d99873259..17e8c08fdc4 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java @@ -29,7 +29,6 @@ public static void main(String[] args) throws Exception { GraphTraversalSource g = graph.traversal(); g.V().drop().iterate(); -// g.E().drop().iterate(); Vertex v1 = g.addV("Concept").property("code", "a").next(); System.out.println(v1.id()); @@ -111,7 +110,6 @@ public static void main(String[] args) throws Exception { System.out.println(""); // Not descendants of 'b' - GraphTraversal graphTraversal = g.V(); graphTraversal = graphTraversal.timeLimit(100L); From 65ed664645766bb537b6721ad1150f515692d9c9 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Fri, 12 Mar 2021 10:14:57 -0500 Subject: [PATCH 22/50] Issue #1980 - changes per PR review Signed-off-by: John T.E. Timm --- docs/src/pages/guides/FHIRServerUsersGuide.md | 10 +++++----- docs/src/pages/guides/FHIRTerminologyGuide.md | 2 ++ .../com/ibm/fhir/config/FHIRConfiguration.java | 6 ++++-- .../com/ibm/fhir/term/graph/FHIRTermGraph.java | 16 ++++++++-------- .../term/graph/factory/FHIRTermGraphFactory.java | 4 ++-- .../term/graph/loader/FHIRTermGraphLoader.java | 8 ++++---- .../factory/FHIRTermGraphLoaderFactory.java | 2 +- .../loader/main/FHIRTermGraphLoaderMain.java | 3 +++ .../fhir/term/graph/util/FHIRTermGraphUtil.java | 10 ++++++---- 9 files changed, 35 insertions(+), 26 deletions(-) diff --git a/docs/src/pages/guides/FHIRServerUsersGuide.md b/docs/src/pages/guides/FHIRServerUsersGuide.md index fb23835d040..2e6fa770ff7 100644 --- a/docs/src/pages/guides/FHIRServerUsersGuide.md +++ b/docs/src/pages/guides/FHIRServerUsersGuide.md @@ -1946,12 +1946,12 @@ This section contains reference information about each of the configuration prop |`fhirServer/core/allowClientHandlingPref`|boolean|Indicates whether the client is allowed to override the server default handling preference using the `Prefer:handling` header value part.| |`fhirServer/core/checkReferenceTypes`|boolean|Indicates whether reference type checking is performed by the server during parsing / deserialization.| |`fhirServer/core/serverRegistryResourceProviderEnabled`|boolean|Indicates whether the server registry resource provider should be used by the FHIR registry component to access definitional resources through the persistence layer.| -|`fhirServer/core/graphTermServiceProvider/enabled`|boolean|Indicates whether the graph term service provider should be used by the FHIR term service to access code system content| -|`fhirServer/core/graphTermServiceProvider/configuration`|object (name/value pairs)|A JSON object that contains the name/value pairs used to configure the graph database behind the graph term service provider see: [https://docs.janusgraph.org/basics/configuration-reference/](https://docs.janusgraph.org/basics/configuration-reference/)| |`fhirServer/core/conditionalDeleteMaxNumber`|integer|The max number of matches supported in conditional delete. | |`fhirServer/core/capabilityStatementCacheTimeout`|integer|The number of minutes that a tenant's CapabilityStatement is cached for the metadata endpoint. | |`fhirServer/core/extendedCodeableConceptValidation`|boolean|A boolean flag which indicates whether extended validation is performed by the server during object construction for code, Coding, CodeableConcept, Quantity, Uri, and String elements which have required bindings to value sets.| |`fhirServer/core/disabledOperations`|string|A comma-separated list of operations which are not allowed to run on the IBM FHIR Server, for example, `validate,import`. Note, do not include the dollar sign `$`| +|`fhirServer/term/graphTermServiceProvider/enabled`|boolean|Indicates whether the graph term service provider should be used by the FHIR term service to access code system content| +|`fhirServer/term/graphTermServiceProvider/configuration`|object (name/value pairs)|A JSON object that contains the name/value pairs used to configure the graph database behind the graph term service provider see: [https://docs.janusgraph.org/basics/configuration-reference/](https://docs.janusgraph.org/basics/configuration-reference/)| |`fhirServer/resources/open`|boolean|Whether resources that are not explicitly listed in the configuration should be supported by the FHIR Server REST layer. When open is set to `false`, only the resources listed in fhir-server-config.json are supported.| |`fhirServer/resources/Resource/interactions`|string list|A list of strings that represent the RESTful interactions (create, read, vread, update, patch, delete, history, and/or search) supported for resource types. Omitting this property is equivalent to supporting all FHIR interactions for the supported resources. An empty list, `[]`, can be used to indicate that no REST methods are supported. This property can be overridden for specific resource types via the `fhirServer/resources//interactions` property.| |`fhirServer/resources/Resource/searchParameters`|object|The set of search parameters to support for all supported resource types. Omitting this property is equivalent to supporting all search parameters in the server's registry that apply to resource type "Resource" (all resources). An empty object, `{}`, can be used to indicate that no global search parameters are supported.| @@ -2053,10 +2053,10 @@ This section contains reference information about each of the configuration prop |`fhirServer/core/allowClientHandlingPref`|true| |`fhirServer/core/checkReferenceTypes`|true| |`fhirServer/core/serverRegistryResourceProviderEnabled`|false| -|`fhirServer/core/graphTermServiceProvider/enabled`|false| |`fhirServer/core/conditionalDeleteMaxNumber`|10| |`fhirServer/core/capabilityStatementCacheTimeout`|60| |`fhirServer/core/extendedCodeableConceptValidation`|true| +|`fhirServer/term/graphTermServiceProvider/enabled`|false| |`fhirServer/resources/open`|true| |`fhirServer/resources/Resource/interactions`|null (all interactions supported)| |`fhirServer/resources/Resource/searchParameters`|null (all global search parameters supported)| @@ -2141,12 +2141,12 @@ must restart the server for that change to take effect. |`fhirServer/core/allowClientHandlingPref`|Y|Y| |`fhirServer/core/checkReferenceTypes`|N|N| |`fhirServer/core/serverRegistryResourceProviderEnabled`|N|N| -|`fhirServer/core/graphTermServiceProvider/enabled`|N|N| -|`fhirServer/core/graphTermServiceProvider/configuration`|N|N| |`fhirServer/core/conditionalDeleteMaxNumber`|Y|Y| |`fhirServer/core/capabilityStatementCacheTimeout`|Y|Y| |`fhirServer/core/extendedCodeableConceptValidation`|N|N| |`fhirServer/core/disabledOperations`|N|N| +|`fhirServer/term/graphTermServiceProvider/enabled`|N|N| +|`fhirServer/term/graphTermServiceProvider/configuration`|N|N| |`fhirServer/resources/open`|Y|Y| |`fhirServer/resources/Resource/interactions`|Y|Y| |`fhirServer/resources/Resource/searchParameters`|Y|Y| diff --git a/docs/src/pages/guides/FHIRTerminologyGuide.md b/docs/src/pages/guides/FHIRTerminologyGuide.md index 3c1117e51b9..ca8a1de2934 100644 --- a/docs/src/pages/guides/FHIRTerminologyGuide.md +++ b/docs/src/pages/guides/FHIRTerminologyGuide.md @@ -102,6 +102,7 @@ Additionally, the FHIRPath functions `subsumedBy` and `subsumes` have been imple The FHIR term graph module [fhir-term-graph](https://github.com/IBM/FHIR/tree/main/fhir-term-graph) provides an implementation of `FHIRTermServiceProvider` that is backed by a graph database ([JanusGraph](https://janusgraph.org)). The module also contains term graph loaders for SNOMED-CT Release Format 2 (RF2) files (SnomedTermGraphLoader), UMLS Rich Release Format (RRF) files (UMLSTermGraphLoader), and FHIR CodeSystem resources (CodeSystemTermGraphLoader). The GraphTermServiceProvider can be enabled through the `fhir-server-config.json` file per the configuration properties specified in the [FHIR Server User's Guide](https://ibm.github.io/FHIR/guides/FHIRServerUsersGuide#51-configuration-properties-reference). Example configuration: ``` json + "term": { "graphTermServiceProvider": { "enabled": true, "configuration": { @@ -115,4 +116,5 @@ The FHIR term graph module [fhir-term-graph](https://github.com/IBM/FHIR/tree/ma "query.fast-property": true } } + } ``` diff --git a/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java b/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java index 2ee1a8e858d..539b373af56 100644 --- a/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java +++ b/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java @@ -33,12 +33,14 @@ public class FHIRConfiguration { public static final String PROPERTY_CHECK_REFERENCE_TYPES = "fhirServer/core/checkReferenceTypes"; public static final String PROPERTY_CONDITIONAL_DELETE_MAX_NUMBER = "fhirServer/core/conditionalDeleteMaxNumber"; public static final String PROPERTY_SERVER_REGISTRY_RESOURCE_PROVIDER_ENABLED = "fhirServer/core/serverRegistryResourceProviderEnabled"; - public static final String PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_ENABLED = "fhirServer/core/graphTermServiceProvider/enabled"; - public static final String PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_CONFIGURATION = "fhirServer/core/graphTermServiceProvider/configuration"; public static final String PROPERTY_CAPABILITY_STATEMENT_CACHE = "fhirServer/core/capabilityStatementCacheTimeout"; public static final String PROPERTY_EXTENDED_CODEABLE_CONCEPT_VALIDATION = "fhirServer/core/extendedCodeableConceptValidation"; public static final String PROPERTY_DISABLED_OPERATIONS = "fhirServer/core/disabledOperations"; + // Terminology service properties + public static final String PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_ENABLED = "fhirServer/term/graphTermServiceProvider/enabled"; + public static final String PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_CONFIGURATION = "fhirServer/term/graphTermServiceProvider/configuration"; + // Resources properties public static final String PROPERTY_RESOURCES = "fhirServer/resources"; public static final String PROPERTY_FIELD_RESOURCES_OPEN = "open"; diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java index 589b5f3a682..b446773c16c 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/FHIRTermGraph.java @@ -24,7 +24,7 @@ public interface FHIRTermGraph { public static final String IS_A = "isa"; /** - * Get the configuration used to create this {@link FHIRTermGraph} + * Get the configuration used to create this {@link FHIRTermGraph}. * * @return * the configuration @@ -32,7 +32,7 @@ public interface FHIRTermGraph { Configuration configuration(); /** - * Get the underlying {@link JanusGraph} instance behind this {@link FHIRTermGraph} + * Get the underlying {@link JanusGraph} instance behind this {@link FHIRTermGraph}. * * @return * the {@link JanusGraph} instance @@ -40,7 +40,7 @@ public interface FHIRTermGraph { JanusGraph getJanusGraph(); /** - * Get the graph traversal source associated with the underlying {@link JanusGraph} instance + * Get the graph traversal source associated with the underlying {@link JanusGraph} instance. * * @return * the graph traversal source @@ -48,7 +48,7 @@ public interface FHIRTermGraph { GraphTraversalSource traversal(); /** - * Query the indexing backend using the Lucene query parser syntax + * Query the indexing backend using the Lucene query parser syntax. * * @param query * the query @@ -61,7 +61,7 @@ default Stream> indexQuery(String query) { /** * Query the indexing backend using the Lucene query parser syntax - * and the provided limit and offset + * and the provided limit and offset. * * @param query * the query @@ -75,17 +75,17 @@ default Stream> indexQuery(String query) { Stream> indexQuery(String query, int limit, int offset); /** - * Close the graph and its underlying resources + * Close the graph and its underlying resources. */ void close(); /** - * Drop the graph + * Drop the graph. */ void drop(); /** - * Drop all vertices and edges from the graph + * Drop all vertices and edges from the graph. */ void dropAllVertices(); } diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/factory/FHIRTermGraphFactory.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/factory/FHIRTermGraphFactory.java index 0e6d90809d6..58288d3e478 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/factory/FHIRTermGraphFactory.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/factory/FHIRTermGraphFactory.java @@ -19,7 +19,7 @@ public final class FHIRTermGraphFactory { private FHIRTermGraphFactory() { } /** - * Create a {@link FHIRTermGraph} instance using the given configuration properties file + * Create a {@link FHIRTermGraph} instance using the given configuration properties file. * * @param propFileName * the configuration properties file @@ -35,7 +35,7 @@ public static FHIRTermGraph open(String propFileName) { } /** - * Create a {@link FHIRTermGraph} instance using the given configuration object + * Create a {@link FHIRTermGraph} instance using the given configuration object. * * @param configuration * the configuration object diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java index c28fd2f0cbd..9686901a0fe 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/FHIRTermGraphLoader.java @@ -50,17 +50,17 @@ public Options options() { } /** - * Load the {@link FHIRTermGraph} + * Load the {@link FHIRTermGraph}. */ void load(); /** - * Close the loader and its underlying resources + * Close the loader and its underlying resources. */ void close(); /** - * Get the options used to create this {@link FHIRTermGraphLoader} + * Get the options used to create this {@link FHIRTermGraphLoader}. * * @return * the options @@ -68,7 +68,7 @@ public Options options() { Map options(); /** - * Get the underlying {@link FHIRTermGraph} instance + * Get the underlying {@link FHIRTermGraph} instance. * * @return * the {@link FHIRTermGraph} instance diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/factory/FHIRTermGraphLoaderFactory.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/factory/FHIRTermGraphLoaderFactory.java index edb432ae587..f94091215d5 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/factory/FHIRTermGraphLoaderFactory.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/factory/FHIRTermGraphLoaderFactory.java @@ -19,7 +19,7 @@ */ public class FHIRTermGraphLoaderFactory { /** - * Create {@link FHIRTermGraphLoader} instance using the provided type and options map + * Create {@link FHIRTermGraphLoader} instance using the provided type and options map. * * @param type * the type diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/main/FHIRTermGraphLoaderMain.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/main/FHIRTermGraphLoaderMain.java index 790fe002855..21b1b3a2ff2 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/main/FHIRTermGraphLoaderMain.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/main/FHIRTermGraphLoaderMain.java @@ -21,6 +21,9 @@ import com.ibm.fhir.term.graph.loader.FHIRTermGraphLoader; import com.ibm.fhir.term.graph.loader.factory.FHIRTermGraphLoaderFactory; +/* + * Main command line entry point for term graph loaders + */ public class FHIRTermGraphLoaderMain { public static void main(String[] args) throws Exception { Options options = null; diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java index 1a413c99ed2..5275111dd5d 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java @@ -33,7 +33,7 @@ public class FHIRTermGraphUtil { private FHIRTermGraphUtil() { } /** - * Convert the given element value to an object value that is compatible with the graph schema + * Convert the given element value to an object value that is compatible with the graph schema. * * @param value * the element value @@ -63,7 +63,7 @@ public static Object toObject(Element value) { } /** - * Normalize the string by making it case and accent insensitive + * Normalize the string by making it case and accent insensitive. * * @param value * the string value to normalized @@ -78,7 +78,9 @@ public static String normalize(String value) { } /** - * Sets the root logger level for the logback classic root logger + * Sets the root logger level for the logback classic root logger. See: + * JanusGraph common questions + * for more information. * * @param level * the level @@ -89,7 +91,7 @@ public static void setRootLoggerLevel(Level level) { } /** - * Convert the {@link DateTime} value to a Long value that is compatible with the graph schema + * Convert the {@link DateTime} value to a Long value that is compatible with the graph schema. * * @param dateTime * the dateTime value From dd0aea4c7066459a85790ab0ab8adb4a3636bf90 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Fri, 12 Mar 2021 13:30:31 -0500 Subject: [PATCH 23/50] Issue #1980 - don't add version to contains if CodeSystem has none Signed-off-by: John T.E. Timm --- .../src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java b/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java index e3334f2394c..15143bcb180 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java @@ -39,6 +39,7 @@ import com.ibm.fhir.model.type.String; import com.ibm.fhir.model.type.Uri; import com.ibm.fhir.registry.FHIRRegistry; +import com.ibm.fhir.registry.resource.FHIRRegistryResource; import com.ibm.fhir.term.service.FHIRTermService; /** @@ -279,7 +280,7 @@ private static Set getContains(Expansion.Contains contains) private static String getLatestVersion(Uri system) { java.lang.String version = FHIRRegistry.getInstance().getLatestVersion(system.getValue(), CodeSystem.class); - return (version != null) ? string(version) : null; + return (version != null && !FHIRRegistryResource.NO_VERSION.toString().equals(version)) ? string(version) : null; } private static boolean hasResource(java.lang.String url, Class resourceType) { From f79a867e3e12230e4b218145d0e1a7cdcc152de1 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Mon, 15 Mar 2021 14:51:51 -0400 Subject: [PATCH 24/50] Issue #1980 - added support for time limit Signed-off-by: John T.E. Timm --- docs/src/pages/guides/FHIRServerUsersGuide.md | 3 + docs/src/pages/guides/FHIRTerminologyGuide.md | 1 + .../ibm/fhir/config/FHIRConfiguration.java | 1 + .../listener/FHIRServletContextListener.java | 4 +- fhir-term-graph/docker/docker-compose.yml | 33 +++++ .../provider/GraphTermServiceProvider.java | 114 ++++++++++++++---- ...GraphTermServiceProviderTimeLimitTest.java | 59 +++++++++ .../exception/FHIRTermServiceException.java | 28 +++++ .../ibm/fhir/term/util/CodeSystemSupport.java | 2 - .../fhir/operation/term/ClosureOperation.java | 3 + .../term/CodeSystemValidateCodeOperation.java | 3 + .../fhir/operation/term/ExpandOperation.java | 3 + .../fhir/operation/term/LookupOperation.java | 5 +- .../operation/term/SubsumesOperation.java | 3 + .../operation/term/TranslateOperation.java | 3 + .../term/ValueSetValidateCodeOperation.java | 3 + 16 files changed, 239 insertions(+), 29 deletions(-) create mode 100644 fhir-term-graph/docker/docker-compose.yml create mode 100644 fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTimeLimitTest.java create mode 100644 fhir-term/src/main/java/com/ibm/fhir/term/service/exception/FHIRTermServiceException.java diff --git a/docs/src/pages/guides/FHIRServerUsersGuide.md b/docs/src/pages/guides/FHIRServerUsersGuide.md index dbfdbe8d05c..e35608b937b 100644 --- a/docs/src/pages/guides/FHIRServerUsersGuide.md +++ b/docs/src/pages/guides/FHIRServerUsersGuide.md @@ -1954,6 +1954,7 @@ This section contains reference information about each of the configuration prop |`fhirServer/core/extendedCodeableConceptValidation`|boolean|A boolean flag which indicates whether extended validation is performed by the server during object construction for code, Coding, CodeableConcept, Quantity, Uri, and String elements which have required bindings to value sets.| |`fhirServer/core/disabledOperations`|string|A comma-separated list of operations which are not allowed to run on the IBM FHIR Server, for example, `validate,import`. Note, do not include the dollar sign `$`| |`fhirServer/term/graphTermServiceProvider/enabled`|boolean|Indicates whether the graph term service provider should be used by the FHIR term service to access code system content| +|`fhirServer/term/graphTermServiceProvider/timeLimit`|integer|Graph traversal time limit (in milliseconds)| |`fhirServer/term/graphTermServiceProvider/configuration`|object (name/value pairs)|A JSON object that contains the name/value pairs used to configure the graph database behind the graph term service provider see: [https://docs.janusgraph.org/basics/configuration-reference/](https://docs.janusgraph.org/basics/configuration-reference/)| |`fhirServer/resources/open`|boolean|Whether resources that are not explicitly listed in the configuration should be supported by the FHIR Server REST layer. When open is set to `false`, only the resources listed in fhir-server-config.json are supported.| |`fhirServer/resources/Resource/interactions`|string list|A list of strings that represent the RESTful interactions (create, read, vread, update, patch, delete, history, and/or search) supported for resource types. Omitting this property is equivalent to supporting all FHIR interactions for the supported resources. An empty list, `[]`, can be used to indicate that no REST methods are supported. This property can be overridden for specific resource types via the `fhirServer/resources//interactions` property.| @@ -2076,6 +2077,7 @@ This section contains reference information about each of the configuration prop |`fhirServer/core/capabilityStatementCacheTimeout`|60| |`fhirServer/core/extendedCodeableConceptValidation`|true| |`fhirServer/term/graphTermServiceProvider/enabled`|false| +|`fhirServer/term/graphTermServiceProvider/timeLimit`|90000| |`fhirServer/resources/open`|true| |`fhirServer/resources/Resource/interactions`|null (all interactions supported)| |`fhirServer/resources/Resource/searchParameters`|null (all global search parameters supported)| @@ -2186,6 +2188,7 @@ must restart the server for that change to take effect. |`fhirServer/core/extendedCodeableConceptValidation`|N|N| |`fhirServer/core/disabledOperations`|N|N| |`fhirServer/term/graphTermServiceProvider/enabled`|N|N| +|`fhirServer/term/graphTermServiceProvider/timeLimit`|N|N| |`fhirServer/term/graphTermServiceProvider/configuration`|N|N| |`fhirServer/resources/open`|Y|Y| |`fhirServer/resources/Resource/interactions`|Y|Y| diff --git a/docs/src/pages/guides/FHIRTerminologyGuide.md b/docs/src/pages/guides/FHIRTerminologyGuide.md index ca8a1de2934..799530a94ad 100644 --- a/docs/src/pages/guides/FHIRTerminologyGuide.md +++ b/docs/src/pages/guides/FHIRTerminologyGuide.md @@ -105,6 +105,7 @@ The FHIR term graph module [fhir-term-graph](https://github.com/IBM/FHIR/tree/ma "term": { "graphTermServiceProvider": { "enabled": true, + "timeLimit": 30000, "configuration": { "storage.backend": "cql", "storage.hostname": "127.0.0.1", diff --git a/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java b/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java index 539b373af56..8cfbb7e073a 100644 --- a/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java +++ b/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java @@ -39,6 +39,7 @@ public class FHIRConfiguration { // Terminology service properties public static final String PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_ENABLED = "fhirServer/term/graphTermServiceProvider/enabled"; + public static final String PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_TIME_LIMIT = "fhirServer/term/graphTermServiceProvider/timeLimit"; public static final String PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_CONFIGURATION = "fhirServer/term/graphTermServiceProvider/configuration"; // Resources properties diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java b/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java index cca6a759e94..2163a7ba337 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java @@ -10,6 +10,7 @@ import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_EXTENDED_CODEABLE_CONCEPT_VALIDATION; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_CONFIGURATION; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_ENABLED; +import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_TIME_LIMIT; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_KAFKA_CONNECTIONPROPS; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_KAFKA_ENABLED; import static com.ibm.fhir.config.FHIRConfiguration.PROPERTY_KAFKA_TOPICNAME; @@ -204,7 +205,8 @@ public void contextInitialized(ServletContextEvent event) { } else { Map map = new HashMap<>(); propertyGroup.getProperties().stream().forEach(entry -> map.put(entry.getName(), entry.getValue())); - graphTermServiceProvider = new GraphTermServiceProvider(new MapConfiguration(map)); + int timeLimit = fhirConfig.getIntProperty(PROPERTY_GRAPH_TERM_SERVICE_PROVIDER_TIME_LIMIT, GraphTermServiceProvider.DEFAULT_TIME_LIMIT); + graphTermServiceProvider = new GraphTermServiceProvider(new MapConfiguration(map), timeLimit); FHIRTermService.getInstance().addProvider(graphTermServiceProvider); } } diff --git a/fhir-term-graph/docker/docker-compose.yml b/fhir-term-graph/docker/docker-compose.yml new file mode 100644 index 00000000000..d0990053e43 --- /dev/null +++ b/fhir-term-graph/docker/docker-compose.yml @@ -0,0 +1,33 @@ +version: "3" + +services: + cassandra: + image: cassandra:3.11.0 + container_name: fhir-cassandra + ports: + - "9042:9042" + - "9160:9160" + networks: + - fhir-network + + elasticsearch: + image: elasticsearch:7.6.2 + container_name: fhir-elasticsearch + environment: + - "ES_JAVA_OPTS=-Xms512m -Xmx512m" + - "http.host=0.0.0.0" + - "network.host=0.0.0.0" + - "transport.host=127.0.0.1" + - "cluster.name=docker-cluster" + - "xpack.security.enabled=false" + - "discovery.zen.minimum_master_nodes=1" + ports: + - "9200:9200" + networks: + - fhir-network + +networks: + fhir-network: +volumes: + fhir-default-data: + 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 545a62864c2..be978095d30 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 @@ -31,6 +31,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.P; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal; import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__; +import org.apache.tinkerpop.gremlin.process.traversal.step.filter.TimeLimitStep; import org.apache.tinkerpop.gremlin.structure.Vertex; import org.janusgraph.core.attribute.Text; @@ -38,46 +39,73 @@ import com.ibm.fhir.model.resource.CodeSystem.Concept; import com.ibm.fhir.model.resource.CodeSystem.Concept.Designation; import com.ibm.fhir.model.resource.CodeSystem.Concept.Property; +import com.ibm.fhir.model.resource.OperationOutcome.Issue; import com.ibm.fhir.model.resource.ValueSet.Compose.Include.Filter; import com.ibm.fhir.model.type.Code; +import com.ibm.fhir.model.type.CodeableConcept; import com.ibm.fhir.model.type.Coding; import com.ibm.fhir.model.type.DateTime; import com.ibm.fhir.model.type.Decimal; import com.ibm.fhir.model.type.Element; import com.ibm.fhir.model.type.Uri; import com.ibm.fhir.model.type.code.CodeSystemHierarchyMeaning; +import com.ibm.fhir.model.type.code.IssueSeverity; +import com.ibm.fhir.model.type.code.IssueType; import com.ibm.fhir.model.type.code.PropertyType; import com.ibm.fhir.term.graph.FHIRTermGraph; import com.ibm.fhir.term.graph.factory.FHIRTermGraphFactory; import com.ibm.fhir.term.graph.util.FHIRTermGraphUtil; +import com.ibm.fhir.term.service.exception.FHIRTermServiceException; import com.ibm.fhir.term.spi.FHIRTermServiceProvider; public class GraphTermServiceProvider implements FHIRTermServiceProvider { + public static final int DEFAULT_TIME_LIMIT = 90000; // 90 seconds private static final int DEFAULT_COUNT = 1000; + private final FHIRTermGraph graph; + private final int timeLimit; public GraphTermServiceProvider(Configuration configuration) { Objects.requireNonNull(configuration, "configuration"); graph = FHIRTermGraphFactory.open(configuration); + timeLimit = DEFAULT_TIME_LIMIT; + } + + public GraphTermServiceProvider(Configuration configuration, int timeLimit) { + Objects.requireNonNull(configuration, "configuration"); + graph = FHIRTermGraphFactory.open(configuration); + this.timeLimit = timeLimit; } public GraphTermServiceProvider(FHIRTermGraph graph) { this.graph = Objects.requireNonNull(graph, "graph"); + timeLimit = DEFAULT_TIME_LIMIT; + } + + public GraphTermServiceProvider(FHIRTermGraph graph, int timeLimit) { + this.graph = Objects.requireNonNull(graph, "graph"); + this.timeLimit = timeLimit; } @Override public Set closure(CodeSystem codeSystem, Code code) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + Set concepts = new LinkedHashSet<>(); concepts.add(getConcept(codeSystem, code, false, false)); - whereCodeSystem(hasCode(vertices(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem) - .repeat(__.in(FHIRTermGraph.IS_A) - .simplePath() - .dedup()) - .emit() - .elementMap() - .toStream() - .forEach(elementMap -> concepts.add(createConcept(elementMap))); + + GraphTraversal g = whereCodeSystem(hasCode(vertices(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem) + .repeat(__.in(FHIRTermGraph.IS_A) + .simplePath() + .dedup()) + .emit() + .timeLimit(timeLimit); + TimeLimitStep timeLimitStep = getTimeLimitStep(g); + + g.elementMap().toStream().forEach(elementMap -> concepts.add(createConcept(elementMap))); + + checkTimeLimit(timeLimitStep); + return concepts; } @@ -90,12 +118,18 @@ public Concept getConcept(CodeSystem codeSystem, Code code) { @Override public Set getConcepts(CodeSystem codeSystem) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + Set concepts = new LinkedHashSet<>(getCount(codeSystem)); - hasVersion(hasUrl(vertices(), codeSystem.getUrl()), codeSystem.getVersion()) - .out("concept") - .elementMap() - .toStream() - .forEach(elementMap -> concepts.add(createConcept(elementMap))); + + GraphTraversal g = hasVersion(hasUrl(vertices(), codeSystem.getUrl()), codeSystem.getVersion()) + .out("concept") + .timeLimit(timeLimit); + TimeLimitStep timeLimitStep = getTimeLimitStep(g); + + g.elementMap().toStream().forEach(elementMap -> concepts.add(createConcept(elementMap))); + + checkTimeLimit(timeLimitStep); + return concepts; } @@ -226,7 +260,12 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { } } - g.hasLabel("Concept").elementMap().toStream().forEach(elementMap -> concepts.add(createConcept(elementMap))); + g = g.hasLabel("Concept").timeLimit(timeLimit); + TimeLimitStep timeLimitStep = getTimeLimitStep(g); + + g.elementMap().toStream().forEach(elementMap -> concepts.add(createConcept(elementMap))); + + checkTimeLimit(timeLimitStep); return concepts; } @@ -235,6 +274,10 @@ public FHIRTermGraph getGraph() { return graph; } + public int getTimeLimit() { + return timeLimit; + } + @Override public boolean hasConcept(CodeSystem codeSystem, Code code) { return whereCodeSystem(hasCode(vertices(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem).hasNext(); @@ -249,15 +292,37 @@ public boolean isSupported(CodeSystem codeSystem) { @Override public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + boolean caseSensitive = isCaseSensitive(codeSystem); + if (codeA.equals(codeB) || (!caseSensitive && normalize(codeA.getValue()).equals(normalize(codeB.getValue())))) { return true; } - return whereCodeSystem(hasCode(vertices(), codeA.getValue(), caseSensitive), codeSystem) - .repeat(__.in(FHIRTermGraph.IS_A) - .simplePath()) - .until(hasCode(codeB.getValue(), caseSensitive)) - .hasNext(); + + GraphTraversal g = whereCodeSystem(hasCode(vertices(), codeA.getValue(), caseSensitive), codeSystem) + .repeat(__.in(FHIRTermGraph.IS_A) + .simplePath()) + .until(hasCode(codeB.getValue(), caseSensitive)) + .timeLimit(timeLimit); + TimeLimitStep timeLimitStep = getTimeLimitStep(g); + + boolean subsumes = g.hasNext(); + + checkTimeLimit(timeLimitStep); + + return subsumes; + } + + private void checkTimeLimit(TimeLimitStep timeLimitStep) { + if (timeLimitStep.getTimedOut()) { + throw new FHIRTermServiceException("Graph traversal timed out", Collections.singletonList(Issue.builder() + .severity(IssueSeverity.ERROR) + .code(IssueType.TOO_COSTLY) + .details(CodeableConcept.builder() + .text(string("Graph traversal timed out")) + .build()) + .build())); + } } private Concept createConcept(CodeSystem codeSystem, String code, Optional> optional, boolean includeDesignations, boolean includeProperties) { @@ -352,7 +417,6 @@ private String getPropertyKey(PropertyType type) { return "valueBoolean"; case CODE: return "valueCode"; -// case CODING: case DATE_TIME: return "valueDateTimeLong"; case DECIMAL: @@ -366,6 +430,11 @@ private String getPropertyKey(PropertyType type) { } } + @SuppressWarnings("rawtypes") + private TimeLimitStep getTimeLimitStep(GraphTraversal g) { + return (TimeLimitStep) g.asAdmin().getEndStep(); + } + private Element getValue(Map elementMap) { if (elementMap.containsKey("valueBoolean")) { return com.ibm.fhir.model.type.Boolean.of((Boolean) elementMap.get("valueBoolean")); @@ -376,11 +445,6 @@ private Element getValue(Map elementMap) { if (elementMap.containsKey("valueDateTime")) { return DateTime.of((String) elementMap.get("valueDateTime")); } - /* - if (elementMap.containsKey("valueDecimal")) { - return Decimal.of((Double) elementMap.get("valueDecimal")); - } - */ if (elementMap.containsKey("valueDecimalString")) { return Decimal.of((String) elementMap.get("valueDecimalString")); } diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTimeLimitTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTimeLimitTest.java new file mode 100644 index 00000000000..ed60f34336d --- /dev/null +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTimeLimitTest.java @@ -0,0 +1,59 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.test; + +import static org.testng.Assert.fail; + +import java.io.InputStream; + +import org.apache.commons.configuration.PropertiesConfiguration; +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.ibm.fhir.model.format.Format; +import com.ibm.fhir.model.parser.FHIRParser; +import com.ibm.fhir.model.resource.CodeSystem; +import com.ibm.fhir.term.graph.FHIRTermGraph; +import com.ibm.fhir.term.graph.factory.FHIRTermGraphFactory; +import com.ibm.fhir.term.graph.loader.FHIRTermGraphLoader; +import com.ibm.fhir.term.graph.loader.impl.CodeSystemTermGraphLoader; +import com.ibm.fhir.term.graph.provider.GraphTermServiceProvider; +import com.ibm.fhir.term.graph.util.FHIRTermGraphUtil; +import com.ibm.fhir.term.service.exception.FHIRTermServiceException; +import com.ibm.fhir.term.spi.FHIRTermServiceProvider; + +import ch.qos.logback.classic.Level; + +public class GraphTermServiceProviderTimeLimitTest { + @Test + public void testGraphTermServiceProviderTimeLimit() throws Exception { + FHIRTermGraphUtil.setRootLoggerLevel(Level.INFO); + + FHIRTermGraph graph = null; + try (InputStream in = GraphTermServiceProviderTimeLimitTest.class.getClassLoader().getResourceAsStream("JSON/CodeSystem-test.json")) { + graph = FHIRTermGraphFactory.open(new PropertiesConfiguration("conf/janusgraph-berkeleyje-lucene.properties")); + graph.dropAllVertices(); + + CodeSystem codeSystem = FHIRParser.parser(Format.JSON).parse(in); + FHIRTermGraphLoader loader = new CodeSystemTermGraphLoader(graph, codeSystem); + loader.load(); + + FHIRTermServiceProvider provider = new GraphTermServiceProvider(graph, 1); + + provider.getConcepts(codeSystem); + + fail(); + } catch (Exception e) { + Assert.assertTrue(e instanceof FHIRTermServiceException); + Assert.assertEquals(e.getMessage(), "Graph traversal timed out"); + } finally { + if (graph != null) { + graph.close(); + } + } + } +} \ No newline at end of file diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/service/exception/FHIRTermServiceException.java b/fhir-term/src/main/java/com/ibm/fhir/term/service/exception/FHIRTermServiceException.java new file mode 100644 index 00000000000..cf3146d29b7 --- /dev/null +++ b/fhir-term/src/main/java/com/ibm/fhir/term/service/exception/FHIRTermServiceException.java @@ -0,0 +1,28 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.service.exception; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import com.ibm.fhir.model.resource.OperationOutcome.Issue; + +public class FHIRTermServiceException extends RuntimeException { + private static final long serialVersionUID = 1L; + + protected final List issues; + + public FHIRTermServiceException(String message, List issues) { + super(message); + this.issues = Collections.unmodifiableList(Objects.requireNonNull(issues, "issues")); + } + + public List getIssues() { + return issues; + } +} 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 fedc9dab5af..65eb0fb82e7 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 @@ -372,7 +372,6 @@ public static Element toElement(String value, PropertyType type) { return Boolean.of(value.getValue()); case CODE: return Code.of(value.getValue()); -// case CODING: case DATE_TIME: return DateTime.of(value.getValue()); case DECIMAL: @@ -403,7 +402,6 @@ public static Element toElement(java.lang.String value, PropertyType type) { return Boolean.of(value); case CODE: return Code.of(value); -// case CODING: case DATE_TIME: return DateTime.of(value); case DECIMAL: diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ClosureOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ClosureOperation.java index 3bcb0794454..015771c9c20 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ClosureOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ClosureOperation.java @@ -37,6 +37,7 @@ import com.ibm.fhir.registry.FHIRRegistry; import com.ibm.fhir.server.operation.spi.FHIROperationContext; import com.ibm.fhir.server.operation.spi.FHIRResourceHelpers; +import com.ibm.fhir.term.service.exception.FHIRTermServiceException; /** * An experimental implementation of the ConceptMap closure operation that does not support versioning or playback @@ -73,6 +74,8 @@ protected Parameters doInvoke( return getOutputParameters(conceptMap); } catch (FHIROperationException e) { throw e; + } catch (FHIRTermServiceException e) { + throw new FHIROperationException(e.getMessage(), e.getCause()).withIssue(e.getIssues()); } catch (Exception e) { throw new FHIROperationException("An error occurred during the ConceptMap closure operation", e); } diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/CodeSystemValidateCodeOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/CodeSystemValidateCodeOperation.java index 815b89068ce..6c804cee6de 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/CodeSystemValidateCodeOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/CodeSystemValidateCodeOperation.java @@ -17,6 +17,7 @@ import com.ibm.fhir.registry.FHIRRegistry; import com.ibm.fhir.server.operation.spi.FHIROperationContext; import com.ibm.fhir.server.operation.spi.FHIRResourceHelpers; +import com.ibm.fhir.term.service.exception.FHIRTermServiceException; import com.ibm.fhir.term.spi.ValidationOutcome; import com.ibm.fhir.term.spi.ValidationParameters; @@ -44,6 +45,8 @@ protected Parameters doInvoke( return outcome.toParameters(); } catch (FHIROperationException e) { throw e; + } catch (FHIRTermServiceException e) { + throw new FHIROperationException(e.getMessage(), e.getCause()).withIssue(e.getIssues()); } catch (Exception e) { throw new FHIROperationException("An error occurred during the CodeSystem validate code operation", e); } diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ExpandOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ExpandOperation.java index 08b084d1ee5..d8cb600a800 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ExpandOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ExpandOperation.java @@ -17,6 +17,7 @@ import com.ibm.fhir.registry.FHIRRegistry; import com.ibm.fhir.server.operation.spi.FHIROperationContext; import com.ibm.fhir.server.operation.spi.FHIRResourceHelpers; +import com.ibm.fhir.term.service.exception.FHIRTermServiceException; import com.ibm.fhir.term.spi.ExpansionParameters; public class ExpandOperation extends AbstractTermOperation { @@ -43,6 +44,8 @@ protected Parameters doInvoke( return getOutputParameters(expanded); } catch (FHIROperationException e) { throw e; + } catch (FHIRTermServiceException e) { + throw new FHIROperationException(e.getMessage(), e.getCause()).withIssue(e.getIssues()); } catch (Exception e) { throw new FHIROperationException("An error occurred during the ValueSet expand operation", e); } diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/LookupOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/LookupOperation.java index 95149468070..d5c5be76771 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/LookupOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/LookupOperation.java @@ -23,6 +23,7 @@ import com.ibm.fhir.server.operation.spi.FHIROperationContext; import com.ibm.fhir.server.operation.spi.FHIRResourceHelpers; import com.ibm.fhir.server.util.FHIRRestHelper; +import com.ibm.fhir.term.service.exception.FHIRTermServiceException; import com.ibm.fhir.term.spi.LookupOutcome; import com.ibm.fhir.term.spi.LookupParameters; @@ -45,7 +46,9 @@ protected Parameters doInvoke( LookupOutcome outcome = null; try { outcome = service.lookup(coding, LookupParameters.from(parameters)); - } catch( Exception e ) { + } catch (FHIRTermServiceException e) { + throw new FHIROperationException(e.getMessage(), e.getCause()).withIssue(e.getIssues()); + } catch(Exception e) { throw new FHIROperationException("An error occurred during the CodeSystem lookup operation", e); } diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/SubsumesOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/SubsumesOperation.java index 5e429c0778c..15c83f371c5 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/SubsumesOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/SubsumesOperation.java @@ -19,6 +19,7 @@ import com.ibm.fhir.registry.FHIRRegistry; import com.ibm.fhir.server.operation.spi.FHIROperationContext; import com.ibm.fhir.server.operation.spi.FHIRResourceHelpers; +import com.ibm.fhir.term.service.exception.FHIRTermServiceException; public class SubsumesOperation extends AbstractTermOperation { @Override @@ -55,6 +56,8 @@ protected Parameters doInvoke( .build(); } catch (FHIROperationException e) { throw e; + } catch (FHIRTermServiceException e) { + throw new FHIROperationException(e.getMessage(), e.getCause()).withIssue(e.getIssues()); } catch (Exception e) { throw new FHIROperationException("An error occurred during the CodeSystem subsumes operation", e); } diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/TranslateOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/TranslateOperation.java index f3034caa2ec..52f6d3bc74a 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/TranslateOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/TranslateOperation.java @@ -17,6 +17,7 @@ import com.ibm.fhir.registry.FHIRRegistry; import com.ibm.fhir.server.operation.spi.FHIROperationContext; import com.ibm.fhir.server.operation.spi.FHIRResourceHelpers; +import com.ibm.fhir.term.service.exception.FHIRTermServiceException; import com.ibm.fhir.term.spi.TranslationOutcome; import com.ibm.fhir.term.spi.TranslationParameters; @@ -43,6 +44,8 @@ protected Parameters doInvoke( return outcome.toParameters(); } catch (FHIROperationException e) { throw e; + } catch (FHIRTermServiceException e) { + throw new FHIROperationException(e.getMessage(), e.getCause()).withIssue(e.getIssues()); } catch (Exception e) { throw new FHIROperationException("An error occurred during the ConceptMap translate operation", e); } diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ValueSetValidateCodeOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ValueSetValidateCodeOperation.java index 9d9bb058b98..e5c901f7b00 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ValueSetValidateCodeOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ValueSetValidateCodeOperation.java @@ -17,6 +17,7 @@ import com.ibm.fhir.registry.FHIRRegistry; import com.ibm.fhir.server.operation.spi.FHIROperationContext; import com.ibm.fhir.server.operation.spi.FHIRResourceHelpers; +import com.ibm.fhir.term.service.exception.FHIRTermServiceException; import com.ibm.fhir.term.spi.ValidationOutcome; import com.ibm.fhir.term.spi.ValidationParameters; @@ -43,6 +44,8 @@ protected Parameters doInvoke( return outcome.toParameters(); } catch (FHIROperationException e) { throw e; + } catch (FHIRTermServiceException e) { + throw new FHIROperationException(e.getMessage(), e.getCause()).withIssue(e.getIssues()); } catch (Exception e) { throw new FHIROperationException("An error occurred during the ValueSet validate code operation", e); } From e1b524dbffc1e60ffc526a7ab4f709c895abffe2 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Mon, 15 Mar 2021 18:02:53 -0400 Subject: [PATCH 25/50] Issue #1980, Issue #2092 - added support for implicit SNOMED value sets Signed-off-by: John T.E. Timm --- .../SnomedImplicitValueSetProvider.java | 117 ++++++++++++++++++ ....registry.spi.FHIRRegistryResourceProvider | 1 + .../SnomedImplicitValueSetProviderTest.java | 26 ++++ .../registry/ValueSetRegistryResource.java | 40 ++++++ 4 files changed, 184 insertions(+) create mode 100644 fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedImplicitValueSetProvider.java create mode 100644 fhir-term-graph/src/main/resources/META-INF/services/com.ibm.fhir.registry.spi.FHIRRegistryResourceProvider create mode 100644 fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedImplicitValueSetProviderTest.java create mode 100644 fhir-term/src/main/java/com/ibm/fhir/term/registry/ValueSetRegistryResource.java diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedImplicitValueSetProvider.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedImplicitValueSetProvider.java new file mode 100644 index 00000000000..e508b5fd064 --- /dev/null +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedImplicitValueSetProvider.java @@ -0,0 +1,117 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.registry; + +import static com.ibm.fhir.core.util.LRUCache.createLRUCache; +import static com.ibm.fhir.model.type.String.string; + +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.model.resource.ValueSet.Compose; +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.Code; +import com.ibm.fhir.model.type.Markdown; +import com.ibm.fhir.model.type.Narrative; +import com.ibm.fhir.model.type.Uri; +import com.ibm.fhir.model.type.Xhtml; +import com.ibm.fhir.model.type.code.FilterOperator; +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.ValueSetRegistryResource; + +public class SnomedImplicitValueSetProvider implements FHIRRegistryResourceProvider { + private static final String SNOMED_IMPLICIT_VALUE_SET_PREFIX = "http://snomed.info/sct?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"; + private static final FHIRRegistryResource SNOMED_ALL_CONCEPTS_VALUE_SET = ValueSetRegistryResource.from(buildAllConceptsValueSet()); + private static final Map SNOMED_IMPLICIT_VALUE_SET_CACHE = createLRUCache(128); + + @Override + public FHIRRegistryResource getRegistryResource(Class resourceType, String url, String version) { + if (ValueSet.class.equals(resourceType) && url.startsWith(SNOMED_IMPLICIT_VALUE_SET_PREFIX)) { + if (SNOMED_IMPLICIT_VALUE_SET_PREFIX.equals(url)) { + return SNOMED_ALL_CONCEPTS_VALUE_SET; + } else { + String[] tokens = url.split("="); + if (tokens.length == 2) { + String sctid = tokens[1]; + return SNOMED_IMPLICIT_VALUE_SET_CACHE.computeIfAbsent(sctid, k -> ValueSetRegistryResource.from(buildImplicitValueSet(sctid))); + } + } + } + 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(); + } + + private ValueSet buildImplicitValueSet(String sctid) { + return ValueSet.builder() + .text(Narrative.builder() + .status(NarrativeStatus.GENERATED) + .div(Xhtml.from("All SNOMED CT concepts subsumed by " + sctid)) + .build()) + .url(Uri.of(SNOMED_IMPLICIT_VALUE_SET_PREFIX + "=" + sctid)) + .name(string("SNOMED CT Concept " + sctid + " and descendants")) + .description(Markdown.of("All SNOMED CT concepts for " + sctid)) + .copyright(Markdown.of(SNOMED_COPYRIGHT)) + .status(PublicationStatus.ACTIVE) + .compose(Compose.builder() + .include(Include.builder() + .system(Uri.of("http://snomed.info/sct")) + .filter(Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IS_A) + .value(string(sctid)) + .build()) + .build()) + .build()) + .build(); + } + + private static ValueSet buildAllConceptsValueSet() { + return ValueSet.builder() + .text(Narrative.builder() + .status(NarrativeStatus.GENERATED) + .div(Xhtml.from("All SNOMED CT concepts")) + .build()) + .url(Uri.of(SNOMED_IMPLICIT_VALUE_SET_PREFIX)) + .name(string("All SNOMED CT concepts")) + .description(Markdown.of("All SNOMED CT concepts")) + .copyright(Markdown.of(SNOMED_COPYRIGHT)) + .status(PublicationStatus.ACTIVE) + .compose(Compose.builder() + .include(Include.builder() + .system(Uri.of("http://snomed.info/sct")) + .build()) + .build()) + .build(); + } +} diff --git a/fhir-term-graph/src/main/resources/META-INF/services/com.ibm.fhir.registry.spi.FHIRRegistryResourceProvider b/fhir-term-graph/src/main/resources/META-INF/services/com.ibm.fhir.registry.spi.FHIRRegistryResourceProvider new file mode 100644 index 00000000000..c91d85475b4 --- /dev/null +++ b/fhir-term-graph/src/main/resources/META-INF/services/com.ibm.fhir.registry.spi.FHIRRegistryResourceProvider @@ -0,0 +1 @@ +com.ibm.fhir.term.graph.registry.SnomedImplicitValueSetProvider \ No newline at end of file diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedImplicitValueSetProviderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedImplicitValueSetProviderTest.java new file mode 100644 index 00000000000..a2ecdeada6c --- /dev/null +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedImplicitValueSetProviderTest.java @@ -0,0 +1,26 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.test; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import com.ibm.fhir.model.resource.ValueSet; +import com.ibm.fhir.registry.FHIRRegistry; + +public class SnomedImplicitValueSetProviderTest { + @Test + public void testSnomedImplicitValueSetProvider() { + ValueSet valueSet = FHIRRegistry.getInstance().getResource("http://snomed.info/sct?fhir_vs", ValueSet.class); + Assert.assertNotNull(valueSet); + System.out.println(valueSet); + + valueSet = FHIRRegistry.getInstance().getResource("http://snomed.info/sct?fhir_vs=195967001", ValueSet.class); + Assert.assertNotNull(valueSet); + System.out.println(valueSet); + } +} diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/registry/ValueSetRegistryResource.java b/fhir-term/src/main/java/com/ibm/fhir/term/registry/ValueSetRegistryResource.java new file mode 100644 index 00000000000..55dde8b5f46 --- /dev/null +++ b/fhir-term/src/main/java/com/ibm/fhir/term/registry/ValueSetRegistryResource.java @@ -0,0 +1,40 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.registry; + +import java.util.logging.Logger; + +import com.ibm.fhir.model.resource.Resource; +import com.ibm.fhir.model.resource.ValueSet; +import com.ibm.fhir.registry.resource.FHIRRegistryResource; + +public class ValueSetRegistryResource extends FHIRRegistryResource { + private static final Logger log = Logger.getLogger(ValueSetRegistryResource.class.getName()); + + protected final ValueSet valueSet; + + private ValueSetRegistryResource(String id, String url, Version version, ValueSet valueSet) { + super(ValueSet.class, id, url, version, null, null); + this.valueSet = valueSet; + } + + @Override + public Resource getResource() { + return valueSet; + } + + public static ValueSetRegistryResource from(ValueSet valueSet) { + String id = valueSet.getId(); + String url = (valueSet.getUrl() != null) ? valueSet.getUrl().getValue() : null; + String version = (valueSet.getVersion() != null) ? valueSet.getVersion().getValue() : null; + if (url == null) { + log.warning(String.format("Could not create ValueSetRegistryResource from ValueSet with: id: %s, url: %s, and version: %s", id, url, version)); + return null; + } + return new ValueSetRegistryResource(id, url, (version != null) ? Version.from(version) : FHIRRegistryResource.NO_VERSION, valueSet); + } +} From 11d9dd27f294abd83dc250e932d1db83b872f18d Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 16 Mar 2021 09:52:57 -0400 Subject: [PATCH 26/50] Issue #1980, Issue #2092 - fixed UnsupportedOperationException issue Signed-off-by: John T.E. Timm --- .../fhir/registry/spi/FHIRRegistryResourceProvider.java | 7 ++++--- .../java/com/ibm/fhir/server/resources/Capabilities.java | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/fhir-registry/src/main/java/com/ibm/fhir/registry/spi/FHIRRegistryResourceProvider.java b/fhir-registry/src/main/java/com/ibm/fhir/registry/spi/FHIRRegistryResourceProvider.java index 657853f5722..ecef145ed7a 100644 --- a/fhir-registry/src/main/java/com/ibm/fhir/registry/spi/FHIRRegistryResourceProvider.java +++ b/fhir-registry/src/main/java/com/ibm/fhir/registry/spi/FHIRRegistryResourceProvider.java @@ -7,6 +7,7 @@ package com.ibm.fhir.registry.spi; import java.util.Collection; +import java.util.Collections; import com.ibm.fhir.model.resource.Resource; import com.ibm.fhir.registry.resource.FHIRRegistryResource; @@ -74,9 +75,9 @@ public interface FHIRRegistryResourceProvider { * Get the profiles for all of the resources. * * @return - * the profile resources from this provider that constrain the resource types + * the profile resources from this provider that constrain the resource types */ - default Collection getProfileResources(){ - throw new UnsupportedOperationException("The specific implementation does not support this feature"); + default Collection getProfileResources() { + return Collections.emptyList(); }; } \ No newline at end of file diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/resources/Capabilities.java b/fhir-server/src/main/java/com/ibm/fhir/server/resources/Capabilities.java index 936a693669d..fe9d19a7a4f 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/resources/Capabilities.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/resources/Capabilities.java @@ -141,7 +141,11 @@ public Response capabilities() { return Response.ok().entity(capabilityStatement).cacheControl(cacheControl).build(); } catch (IllegalArgumentException e) { FHIROperationException foe = buildRestException(ERROR_CONSTRUCTING, IssueType.EXCEPTION); - log.log(Level.SEVERE, ERROR_MSG, foe); + if (e.getCause() != null) { + log.log(Level.SEVERE, ERROR_MSG, e.getCause()); + } else { + log.log(Level.SEVERE, ERROR_MSG, foe); + } return exceptionResponse(e, issueListToStatus(foe.getIssues())); } catch (Exception e) { log.log(Level.SEVERE, ERROR_MSG, e); From 44941bc534f5c990e0b7b02c6a7f87aa168de8d7 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 16 Mar 2021 12:04:07 -0400 Subject: [PATCH 27/50] Issue #1980, Issue #2092 - additional updates / clean-up Signed-off-by: John T.E. Timm --- ...va => SnomedRegistryResourceProvider.java} | 84 ++- ....registry.spi.FHIRRegistryResourceProvider | 2 +- .../resources/snomed/codesystem-snomedct.json | 493 ++++++++++++++++++ .../SnomedImplicitValueSetProviderTest.java | 26 - .../SnomedRegistryResourceProviderTest.java | 87 ++++ .../registry/ValueSetRegistryResource.java | 40 -- .../resource/FHIRTermRegistryResource.java | 56 ++ .../fhir/term/service/FHIRTermService.java | 1 + 8 files changed, 698 insertions(+), 91 deletions(-) rename fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/{SnomedImplicitValueSetProvider.java => SnomedRegistryResourceProvider.java} (62%) create mode 100644 fhir-term-graph/src/main/resources/snomed/codesystem-snomedct.json delete mode 100644 fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedImplicitValueSetProviderTest.java create mode 100644 fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedRegistryResourceProviderTest.java delete mode 100644 fhir-term/src/main/java/com/ibm/fhir/term/registry/ValueSetRegistryResource.java create mode 100644 fhir-term/src/main/java/com/ibm/fhir/term/registry/resource/FHIRTermRegistryResource.java diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedImplicitValueSetProvider.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedRegistryResourceProvider.java similarity index 62% rename from fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedImplicitValueSetProvider.java rename to fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedRegistryResourceProvider.java index e508b5fd064..0348ec3bb9e 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedImplicitValueSetProvider.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/registry/SnomedRegistryResourceProvider.java @@ -9,15 +9,22 @@ import static com.ibm.fhir.core.util.LRUCache.createLRUCache; import static com.ibm.fhir.model.type.String.string; +import java.io.InputStream; import java.util.Collection; import java.util.Collections; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import com.ibm.fhir.model.format.Format; +import com.ibm.fhir.model.parser.FHIRParser; +import com.ibm.fhir.model.resource.CodeSystem; import com.ibm.fhir.model.resource.Resource; import com.ibm.fhir.model.resource.ValueSet; import com.ibm.fhir.model.resource.ValueSet.Compose; 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.Markdown; import com.ibm.fhir.model.type.Narrative; @@ -28,26 +35,47 @@ 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.ValueSetRegistryResource; +import com.ibm.fhir.term.registry.resource.FHIRTermRegistryResource; +import com.ibm.fhir.term.service.FHIRTermService; +import com.ibm.fhir.term.spi.ValidationOutcome; -public class SnomedImplicitValueSetProvider implements FHIRRegistryResourceProvider { - private static final String SNOMED_IMPLICIT_VALUE_SET_PREFIX = "http://snomed.info/sct?fhir_vs"; +public class SnomedRegistryResourceProvider implements FHIRRegistryResourceProvider { + private static final Logger log = Logger.getLogger(SnomedRegistryResourceProvider.class.getName()); + + 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"; - private static final FHIRRegistryResource SNOMED_ALL_CONCEPTS_VALUE_SET = ValueSetRegistryResource.from(buildAllConceptsValueSet()); - private static final Map SNOMED_IMPLICIT_VALUE_SET_CACHE = createLRUCache(128); + + public static final CodeSystem SNOMED_CODE_SYSTEM = loadCodeSystem(); + private static final FHIRRegistryResource SNOMED_CODE_SYSTEM_REGISTRY_RESOURCE = FHIRTermRegistryResource.from(SNOMED_CODE_SYSTEM); + private static final FHIRRegistryResource SNOMED_ALL_CONCEPTS_IMPLICIT_VALUE_SET_REGISTRY_RESOURCE = FHIRTermRegistryResource.from(buildAllConceptsImplicitValueSet()); + private static final Map SNOMED_SUBSUMED_BY_IMPLICIT_VALUE_SET_REGISTRY_RESOURCE_CACHE = createLRUCache(128); @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_VALUE_SET; + return SNOMED_ALL_CONCEPTS_IMPLICIT_VALUE_SET_REGISTRY_RESOURCE; } else { String[] tokens = url.split("="); if (tokens.length == 2) { String sctid = tokens[1]; - return SNOMED_IMPLICIT_VALUE_SET_CACHE.computeIfAbsent(sctid, k -> ValueSetRegistryResource.from(buildImplicitValueSet(sctid))); + ValidationOutcome outcome = FHIRTermService.getInstance().validateCode(SNOMED_CODE_SYSTEM, null, Code.of(sctid), null); + if (outcome == null || (Boolean.FALSE.equals(outcome.getResult()))) { + if (outcome != null) { + System.out.println("outcome: " + outcome.toParameters()); + } + 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 -> FHIRTermRegistryResource.from(buildSubsumedByImplicitValueSet(sctid))); } } + } else if (CodeSystem.class.equals(resourceType) && SNOMED_URL.equals(url)) { + return SNOMED_CODE_SYSTEM_REGISTRY_RESOURCE; } return null; } @@ -72,46 +100,54 @@ public Collection getSearchParameterResources(String type) return Collections.emptyList(); } - private ValueSet buildImplicitValueSet(String sctid) { + private static ValueSet buildAllConceptsImplicitValueSet() { return ValueSet.builder() .text(Narrative.builder() .status(NarrativeStatus.GENERATED) - .div(Xhtml.from("All SNOMED CT concepts subsumed by " + sctid)) + .div(Xhtml.from("All SNOMED CT concepts")) .build()) - .url(Uri.of(SNOMED_IMPLICIT_VALUE_SET_PREFIX + "=" + sctid)) - .name(string("SNOMED CT Concept " + sctid + " and descendants")) - .description(Markdown.of("All SNOMED CT concepts for " + sctid)) + .url(Uri.of(SNOMED_IMPLICIT_VALUE_SET_PREFIX)) + .name(string("All SNOMED CT concepts")) + .description(Markdown.of("All SNOMED CT concepts")) .copyright(Markdown.of(SNOMED_COPYRIGHT)) .status(PublicationStatus.ACTIVE) .compose(Compose.builder() .include(Include.builder() - .system(Uri.of("http://snomed.info/sct")) - .filter(Filter.builder() - .property(Code.of("concept")) - .op(FilterOperator.IS_A) - .value(string(sctid)) - .build()) + .system(Uri.of(SNOMED_URL)) .build()) .build()) .build(); } - private static ValueSet buildAllConceptsValueSet() { + private ValueSet buildSubsumedByImplicitValueSet(String sctid) { return ValueSet.builder() .text(Narrative.builder() .status(NarrativeStatus.GENERATED) - .div(Xhtml.from("All SNOMED CT concepts")) + .div(Xhtml.from("All SNOMED CT concepts subsumed by " + sctid)) .build()) - .url(Uri.of(SNOMED_IMPLICIT_VALUE_SET_PREFIX)) - .name(string("All SNOMED CT concepts")) - .description(Markdown.of("All SNOMED CT concepts")) + .url(Uri.of(SNOMED_IMPLICIT_VALUE_SET_PREFIX + "=" + sctid)) + .name(string("SNOMED CT Concept " + sctid + " and descendants")) + .description(Markdown.of("All SNOMED CT concepts for " + sctid)) .copyright(Markdown.of(SNOMED_COPYRIGHT)) .status(PublicationStatus.ACTIVE) .compose(Compose.builder() .include(Include.builder() - .system(Uri.of("http://snomed.info/sct")) + .system(Uri.of(SNOMED_URL)) + .filter(Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IS_A) + .value(string(sctid)) + .build()) .build()) .build()) .build(); } + + private static CodeSystem loadCodeSystem() { + try (InputStream in = SnomedRegistryResourceProvider.class.getClassLoader().getResourceAsStream("snomed/codesystem-snomedct.json")) { + return FHIRParser.parser(Format.JSON).parse(in); + } catch (Exception e) { + throw new Error(e); + } + } } diff --git a/fhir-term-graph/src/main/resources/META-INF/services/com.ibm.fhir.registry.spi.FHIRRegistryResourceProvider b/fhir-term-graph/src/main/resources/META-INF/services/com.ibm.fhir.registry.spi.FHIRRegistryResourceProvider index c91d85475b4..60faf4bc9a1 100644 --- a/fhir-term-graph/src/main/resources/META-INF/services/com.ibm.fhir.registry.spi.FHIRRegistryResourceProvider +++ b/fhir-term-graph/src/main/resources/META-INF/services/com.ibm.fhir.registry.spi.FHIRRegistryResourceProvider @@ -1 +1 @@ -com.ibm.fhir.term.graph.registry.SnomedImplicitValueSetProvider \ No newline at end of file +com.ibm.fhir.term.graph.registry.SnomedRegistryResourceProvider \ No newline at end of file diff --git a/fhir-term-graph/src/main/resources/snomed/codesystem-snomedct.json b/fhir-term-graph/src/main/resources/snomed/codesystem-snomedct.json new file mode 100644 index 00000000000..d8ab93933ff --- /dev/null +++ b/fhir-term-graph/src/main/resources/snomed/codesystem-snomedct.json @@ -0,0 +1,493 @@ +{ + "resourceType": "CodeSystem", + "id": "snomedct", + "text": { + "status": "generated", + "div": "
\n

SNOMED CT (all versions)

\n
\n

SNOMED CT is the most comprehensive and precise clinical health terminology product in the world, owned and distributed around the world by The International Health Terminology Standards Development Organisation (IHTSDO).

\n\n
\n

\n Copyright Statement:\n

\n
\n

© 2002-2016 International Health Terminology Standards Development Organisation (IHTSDO). All rights reserved. SNOMED CT®, was originally created by The College of American Pathologists. "SNOMED" and "SNOMED CT" are registered trademarks of the IHTSDO http://www.ihtsdo.org/snomed-ct/get-snomed-ct

\n\n
\n

\n Properties\n

\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n Code\n \n URL\n \n Description\n \n Type\n
inactivehttp://snomed.info/field/Concept.activeWhether the code is active or not (defaults to false). Not the same as deprecatedboolean
definitionStatusIdhttp://snomed.info/field/Concept.definitionStatusIdEither of the codes that are descendants of 900000000000444006code
parenthttp://.........?A SNOMED CT concept id that has the target of a direct is-a relationship from the conceptcode
childhttp://.........?A SNOMED CT concept id that has a direct is-a relationship to the conceptcode
moduleIdhttp://snomed.info/field/Concept.moduleIdThe SNOMED CT concept id of the module that the concept belongs to.code
normalFormhttp://.........?Generated Normal form expression for the provided code or expression, with termsstring
normalFormTersehttp://.........?Generated Normal form expression for the provided code or expression, conceptIds onlystring
Due tohttp://snomed.info/id/42752001\n code
Associated withhttp://snomed.info/id/47429007\n code
Associated morphologyhttp://snomed.info/id/116676008\n code
Has specimenhttp://snomed.info/id/116686009\n code
Specimen source morphologyhttp://snomed.info/id/118168003\n code
Specimen source topographyhttp://snomed.info/id/118169006\n code
Specimen source identityhttp://snomed.info/id/118170007\n code
Specimen procedurehttp://snomed.info/id/118171006\n code
Part ofhttp://snomed.info/id/123005000\n code
Has active ingredienthttp://snomed.info/id/127489000\n code
Subject of informationhttp://snomed.info/id/131195008\n code
Causative agenthttp://snomed.info/id/246075003\n code
Associated findinghttp://snomed.info/id/246090004\n code
Componenthttp://snomed.info/id/246093002\n code
Severityhttp://snomed.info/id/246112005\n code
Occurrencehttp://snomed.info/id/246454002\n code
Episodicityhttp://snomed.info/id/246456000\n code
Techniquehttp://snomed.info/id/246501002\n code
Revision statushttp://snomed.info/id/246513007\n code
Unitshttp://snomed.info/id/246514001\n code
Afterhttp://snomed.info/id/255234002\n code
Accesshttp://snomed.info/id/260507000\n code
Methodhttp://snomed.info/id/260686004\n code
Priorityhttp://snomed.info/id/260870009\n code
Clinical coursehttp://snomed.info/id/263502005\n code
Lateralityhttp://snomed.info/id/272741003\n code
Associated procedurehttp://snomed.info/id/363589002\n code
Finding sitehttp://snomed.info/id/363698007\n code
Lateralityhttp://snomed.info/id/363699004\n code
Direct morphologyhttp://snomed.info/id/363700003\n code
Direct substancehttp://snomed.info/id/363701004\n code
Has focushttp://snomed.info/id/363702006\n code
Has intenthttp://snomed.info/id/363703001\n code
Procedure sitehttp://snomed.info/id/363704007\n code
Has definitional manifestationhttp://snomed.info/id/363705008\n code
Indirect morphologyhttp://snomed.info/id/363709002\n code
Indirect devicehttp://snomed.info/id/363710007\n code
Has interpretationhttp://snomed.info/id/363713009\n code
Interpretshttp://snomed.info/id/363714003\n code
Measurement methodhttp://snomed.info/id/370129005\n code
Propertyhttp://snomed.info/id/370130000\n code
Recipient categoryhttp://snomed.info/id/370131001\n code
Scale typehttp://snomed.info/id/370132008\n code
Specimen substancehttp://snomed.info/id/370133003\n code
Time aspecthttp://snomed.info/id/370134009\n code
Pathological processhttp://snomed.info/id/370135005\n code
Procedure site - Directhttp://snomed.info/id/405813007\n code
Procedure site - Indirecthttp://snomed.info/id/405814001\n code
Procedure devicehttp://snomed.info/id/405815000\n code
Procedure morphologyhttp://snomed.info/id/405816004\n code
Finding contexthttp://snomed.info/id/408729009\n code
Procedure contexthttp://snomed.info/id/408730004\n code
Temporal contexthttp://snomed.info/id/408731000\n code
Subject relationship contexthttp://snomed.info/id/408732007\n code
Route of administrationhttp://snomed.info/id/410675002\n code
Has dose formhttp://snomed.info/id/411116001\n code
Finding methodhttp://snomed.info/id/418775008\n code
Finding informerhttp://snomed.info/id/419066007\n code
Using devicehttp://snomed.info/id/424226004\n code
Using energyhttp://snomed.info/id/424244007\n code
Using substancehttp://snomed.info/id/424361007\n code
Surgical approachhttp://snomed.info/id/424876005\n code
Using access devicehttp://snomed.info/id/425391005\n code
Role grouphttp://snomed.info/id/609096000\n code
Property typehttp://snomed.info/id/704318007\n code
Inheres inhttp://snomed.info/id/704319004\n code
Towardshttp://snomed.info/id/704320005\n code
Characterizeshttp://snomed.info/id/704321009\n code
Process agenthttp://snomed.info/id/704322002\n code
Process durationhttp://snomed.info/id/704323007\n code
Process outputhttp://snomed.info/id/704324001\n code
Relative tohttp://snomed.info/id/704325000\n code
Preconditionhttp://snomed.info/id/704326004\n code
Direct sitehttp://snomed.info/id/704327008\n code
Specified byhttp://snomed.info/id/704346009\n code
Observeshttp://snomed.info/id/704347000\n code
Is abouthttp://snomed.info/id/704647008\n code
\n

\n Filters\n

\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n Code\n \n Description\n \n operator\n \n Value\n
conceptFilter that includes concepts based on their logical definition. e.g. [concept] [is-a] [x] - include all concepts with an is-a relationship to concept x, or [concept] [in] [x]- include all concepts in the reference set identified by concept xis-a in A SNOMED CT code
expressionThe result of the filter is the result of executing the given SNOMED CT Expression Constraint= A SNOMED CT ECL expression (see http://snomed.org/ecl)
expressionsWhether post-coordinated expressions are included in the value set= true or false
\n

This code system http://snomed.info/sct defines many codes, but they are not represented here

\n
" + }, + "url": "http://snomed.info/sct", + "identifier": [ + { + "system": "urn:ietf:rfc:3986", + "value": "urn:oid:2.16.840.1.113883.6.96" + } + ], + "name": "SNOMED_CT", + "title": "SNOMED CT (all versions)", + "status": "active", + "experimental": false, + "publisher": "IHTSDO", + "contact": [ + { + "telecom": [ + { + "system": "url", + "value": "http://ihtsdo.org" + } + ] + } + ], + "description": "SNOMED CT is the most comprehensive and precise clinical health terminology product in the world, owned and distributed around the world by The International Health Terminology Standards Development Organisation (IHTSDO).", + "copyright": "© 2002-2016 International Health Terminology Standards Development Organisation (IHTSDO). All rights reserved. SNOMED CT®, was originally created by The College of American Pathologists. \"SNOMED\" and \"SNOMED CT\" are registered trademarks of the IHTSDO http://www.ihtsdo.org/snomed-ct/get-snomed-ct", + "caseSensitive": false, + "hierarchyMeaning": "is-a", + "compositional": true, + "versionNeeded": false, + "content": "not-present", + "filter": [ + { + "code": "concept", + "description": "Filter that includes concepts based on their logical definition. e.g. [concept] [is-a] [x] - include all concepts with an is-a relationship to concept x, or [concept] [in] [x]- include all concepts in the reference set identified by concept x", + "operator": [ + "is-a", + "in" + ], + "value": "A SNOMED CT code" + }, + { + "code": "expression", + "description": "The result of the filter is the result of executing the given SNOMED CT Expression Constraint", + "operator": [ + "=" + ], + "value": "A SNOMED CT ECL expression (see http://snomed.org/ecl)" + }, + { + "code": "expressions", + "description": "Whether post-coordinated expressions are included in the value set", + "operator": [ + "=" + ], + "value": "true or false" + } + ], + "property": [ + { + "code": "inactive", + "uri": "http://snomed.info/field/Concept.active", + "description": "Whether the code is active or not (defaults to false). Not the same as deprecated", + "type": "boolean" + }, + { + "code": "definitionStatusId", + "uri": "http://snomed.info/field/Concept.definitionStatusId", + "description": "Either of the codes that are descendants of 900000000000444006", + "type": "code" + }, + { + "code": "parent", + "uri": "http://.........?", + "description": "A SNOMED CT concept id that has the target of a direct is-a relationship from the concept", + "type": "code" + }, + { + "code": "child", + "uri": "http://.........?", + "description": "A SNOMED CT concept id that has a direct is-a relationship to the concept", + "type": "code" + }, + { + "code": "moduleId", + "uri": "http://snomed.info/field/Concept.moduleId", + "description": "The SNOMED CT concept id of the module that the concept belongs to.", + "type": "code" + }, + { + "code": "normalForm", + "uri": "http://.........?", + "description": "Generated Normal form expression for the provided code or expression, with terms", + "type": "string" + }, + { + "code": "normalFormTerse", + "uri": "http://.........?", + "description": "Generated Normal form expression for the provided code or expression, conceptIds only", + "type": "string" + }, + { + "code": "Due to", + "uri": "http://snomed.info/id/42752001", + "type": "code" + }, + { + "code": "Associated with", + "uri": "http://snomed.info/id/47429007", + "type": "code" + }, + { + "code": "Associated morphology", + "uri": "http://snomed.info/id/116676008", + "type": "code" + }, + { + "code": "Has specimen", + "uri": "http://snomed.info/id/116686009", + "type": "code" + }, + { + "code": "Specimen source morphology", + "uri": "http://snomed.info/id/118168003", + "type": "code" + }, + { + "code": "Specimen source topography", + "uri": "http://snomed.info/id/118169006", + "type": "code" + }, + { + "code": "Specimen source identity", + "uri": "http://snomed.info/id/118170007", + "type": "code" + }, + { + "code": "Specimen procedure", + "uri": "http://snomed.info/id/118171006", + "type": "code" + }, + { + "code": "Part of", + "uri": "http://snomed.info/id/123005000", + "type": "code" + }, + { + "code": "Has active ingredient", + "uri": "http://snomed.info/id/127489000", + "type": "code" + }, + { + "code": "Subject of information", + "uri": "http://snomed.info/id/131195008", + "type": "code" + }, + { + "code": "Causative agent", + "uri": "http://snomed.info/id/246075003", + "type": "code" + }, + { + "code": "Associated finding", + "uri": "http://snomed.info/id/246090004", + "type": "code" + }, + { + "code": "Component", + "uri": "http://snomed.info/id/246093002", + "type": "code" + }, + { + "code": "Severity", + "uri": "http://snomed.info/id/246112005", + "type": "code" + }, + { + "code": "Occurrence", + "uri": "http://snomed.info/id/246454002", + "type": "code" + }, + { + "code": "Episodicity", + "uri": "http://snomed.info/id/246456000", + "type": "code" + }, + { + "code": "Technique", + "uri": "http://snomed.info/id/246501002", + "type": "code" + }, + { + "code": "Revision status", + "uri": "http://snomed.info/id/246513007", + "type": "code" + }, + { + "code": "Units", + "uri": "http://snomed.info/id/246514001", + "type": "code" + }, + { + "code": "After", + "uri": "http://snomed.info/id/255234002", + "type": "code" + }, + { + "code": "Access", + "uri": "http://snomed.info/id/260507000", + "type": "code" + }, + { + "code": "Method", + "uri": "http://snomed.info/id/260686004", + "type": "code" + }, + { + "code": "Priority", + "uri": "http://snomed.info/id/260870009", + "type": "code" + }, + { + "code": "Clinical course", + "uri": "http://snomed.info/id/263502005", + "type": "code" + }, + { + "code": "Laterality", + "uri": "http://snomed.info/id/272741003", + "type": "code" + }, + { + "code": "Associated procedure", + "uri": "http://snomed.info/id/363589002", + "type": "code" + }, + { + "code": "Finding site", + "uri": "http://snomed.info/id/363698007", + "type": "code" + }, + { + "code": "Laterality", + "uri": "http://snomed.info/id/363699004", + "type": "code" + }, + { + "code": "Direct morphology", + "uri": "http://snomed.info/id/363700003", + "type": "code" + }, + { + "code": "Direct substance", + "uri": "http://snomed.info/id/363701004", + "type": "code" + }, + { + "code": "Has focus", + "uri": "http://snomed.info/id/363702006", + "type": "code" + }, + { + "code": "Has intent", + "uri": "http://snomed.info/id/363703001", + "type": "code" + }, + { + "code": "Procedure site", + "uri": "http://snomed.info/id/363704007", + "type": "code" + }, + { + "code": "Has definitional manifestation", + "uri": "http://snomed.info/id/363705008", + "type": "code" + }, + { + "code": "Indirect morphology", + "uri": "http://snomed.info/id/363709002", + "type": "code" + }, + { + "code": "Indirect device", + "uri": "http://snomed.info/id/363710007", + "type": "code" + }, + { + "code": "Has interpretation", + "uri": "http://snomed.info/id/363713009", + "type": "code" + }, + { + "code": "Interprets", + "uri": "http://snomed.info/id/363714003", + "type": "code" + }, + { + "code": "Measurement method", + "uri": "http://snomed.info/id/370129005", + "type": "code" + }, + { + "code": "Property", + "uri": "http://snomed.info/id/370130000", + "type": "code" + }, + { + "code": "Recipient category", + "uri": "http://snomed.info/id/370131001", + "type": "code" + }, + { + "code": "Scale type", + "uri": "http://snomed.info/id/370132008", + "type": "code" + }, + { + "code": "Specimen substance", + "uri": "http://snomed.info/id/370133003", + "type": "code" + }, + { + "code": "Time aspect", + "uri": "http://snomed.info/id/370134009", + "type": "code" + }, + { + "code": "Pathological process", + "uri": "http://snomed.info/id/370135005", + "type": "code" + }, + { + "code": "Procedure site - Direct", + "uri": "http://snomed.info/id/405813007", + "type": "code" + }, + { + "code": "Procedure site - Indirect", + "uri": "http://snomed.info/id/405814001", + "type": "code" + }, + { + "code": "Procedure device", + "uri": "http://snomed.info/id/405815000", + "type": "code" + }, + { + "code": "Procedure morphology", + "uri": "http://snomed.info/id/405816004", + "type": "code" + }, + { + "code": "Finding context", + "uri": "http://snomed.info/id/408729009", + "type": "code" + }, + { + "code": "Procedure context", + "uri": "http://snomed.info/id/408730004", + "type": "code" + }, + { + "code": "Temporal context", + "uri": "http://snomed.info/id/408731000", + "type": "code" + }, + { + "code": "Subject relationship context", + "uri": "http://snomed.info/id/408732007", + "type": "code" + }, + { + "code": "Route of administration", + "uri": "http://snomed.info/id/410675002", + "type": "code" + }, + { + "code": "Has dose form", + "uri": "http://snomed.info/id/411116001", + "type": "code" + }, + { + "code": "Finding method", + "uri": "http://snomed.info/id/418775008", + "type": "code" + }, + { + "code": "Finding informer", + "uri": "http://snomed.info/id/419066007", + "type": "code" + }, + { + "code": "Using device", + "uri": "http://snomed.info/id/424226004", + "type": "code" + }, + { + "code": "Using energy", + "uri": "http://snomed.info/id/424244007", + "type": "code" + }, + { + "code": "Using substance", + "uri": "http://snomed.info/id/424361007", + "type": "code" + }, + { + "code": "Surgical approach", + "uri": "http://snomed.info/id/424876005", + "type": "code" + }, + { + "code": "Using access device", + "uri": "http://snomed.info/id/425391005", + "type": "code" + }, + { + "code": "Role group", + "uri": "http://snomed.info/id/609096000", + "type": "code" + }, + { + "code": "Property type", + "uri": "http://snomed.info/id/704318007", + "type": "code" + }, + { + "code": "Inheres in", + "uri": "http://snomed.info/id/704319004", + "type": "code" + }, + { + "code": "Towards", + "uri": "http://snomed.info/id/704320005", + "type": "code" + }, + { + "code": "Characterizes", + "uri": "http://snomed.info/id/704321009", + "type": "code" + }, + { + "code": "Process agent", + "uri": "http://snomed.info/id/704322002", + "type": "code" + }, + { + "code": "Process duration", + "uri": "http://snomed.info/id/704323007", + "type": "code" + }, + { + "code": "Process output", + "uri": "http://snomed.info/id/704324001", + "type": "code" + }, + { + "code": "Relative to", + "uri": "http://snomed.info/id/704325000", + "type": "code" + }, + { + "code": "Precondition", + "uri": "http://snomed.info/id/704326004", + "type": "code" + }, + { + "code": "Direct site", + "uri": "http://snomed.info/id/704327008", + "type": "code" + }, + { + "code": "Specified by", + "uri": "http://snomed.info/id/704346009", + "type": "code" + }, + { + "code": "Observes", + "uri": "http://snomed.info/id/704347000", + "type": "code" + }, + { + "code": "Is about", + "uri": "http://snomed.info/id/704647008", + "type": "code" + } + ] +} \ No newline at end of file diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedImplicitValueSetProviderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedImplicitValueSetProviderTest.java deleted file mode 100644 index a2ecdeada6c..00000000000 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedImplicitValueSetProviderTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2021 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.ibm.fhir.term.graph.test; - -import org.testng.Assert; -import org.testng.annotations.Test; - -import com.ibm.fhir.model.resource.ValueSet; -import com.ibm.fhir.registry.FHIRRegistry; - -public class SnomedImplicitValueSetProviderTest { - @Test - public void testSnomedImplicitValueSetProvider() { - ValueSet valueSet = FHIRRegistry.getInstance().getResource("http://snomed.info/sct?fhir_vs", ValueSet.class); - Assert.assertNotNull(valueSet); - System.out.println(valueSet); - - valueSet = FHIRRegistry.getInstance().getResource("http://snomed.info/sct?fhir_vs=195967001", ValueSet.class); - Assert.assertNotNull(valueSet); - System.out.println(valueSet); - } -} diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedRegistryResourceProviderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedRegistryResourceProviderTest.java new file mode 100644 index 00000000000..83fa9ec0ec4 --- /dev/null +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/SnomedRegistryResourceProviderTest.java @@ -0,0 +1,87 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.graph.test; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import com.ibm.fhir.model.resource.CodeSystem; +import com.ibm.fhir.model.resource.CodeSystem.Concept; +import com.ibm.fhir.model.resource.ValueSet; +import com.ibm.fhir.model.resource.ValueSet.Compose.Include.Filter; +import com.ibm.fhir.model.type.Code; +import com.ibm.fhir.registry.FHIRRegistry; +import com.ibm.fhir.term.graph.registry.SnomedRegistryResourceProvider; +import com.ibm.fhir.term.service.FHIRTermService; +import com.ibm.fhir.term.spi.FHIRTermServiceProvider; + +public class SnomedRegistryResourceProviderTest { + @BeforeClass + public void beforeClass() { + FHIRTermService.getInstance().addProvider(new FHIRTermServiceProvider() { + @Override + public Set closure(CodeSystem codeSystem, Code code) { + return Collections.emptySet(); + } + + @Override + public Concept getConcept(CodeSystem codeSystem, Code code) { + return Concept.builder() + .code(code) + .build(); + } + + @Override + public Set getConcepts(CodeSystem codeSystem) { + return Collections.emptySet(); + } + + @Override + public Set getConcepts(CodeSystem codeSystem, List filters) { + return Collections.emptySet(); + } + + @Override + public boolean hasConcept(CodeSystem codeSystem, Code code) { + return true; + } + + @Override + public boolean isSupported(CodeSystem codeSystem) { + return SnomedRegistryResourceProvider.SNOMED_CODE_SYSTEM.equals(codeSystem); + } + + @Override + public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { + return false; + } + }); + } + + @Test + public void testGetAllConceptsImplicitValueSet() { + ValueSet valueSet = FHIRRegistry.getInstance().getResource("http://snomed.info/sct?fhir_vs", ValueSet.class); + Assert.assertNotNull(valueSet); + } + + @Test + public void testGetSubsumedByImplicitValueSet() { + ValueSet valueSet = FHIRRegistry.getInstance().getResource("http://snomed.info/sct?fhir_vs=195967001", ValueSet.class); + Assert.assertNotNull(valueSet); + } + + @Test + public void testGetCodeSystem() { + CodeSystem codeSystem = FHIRRegistry.getInstance().getResource("http://snomed.info/sct", CodeSystem.class); + Assert.assertNotNull(codeSystem); + } +} diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/registry/ValueSetRegistryResource.java b/fhir-term/src/main/java/com/ibm/fhir/term/registry/ValueSetRegistryResource.java deleted file mode 100644 index 55dde8b5f46..00000000000 --- a/fhir-term/src/main/java/com/ibm/fhir/term/registry/ValueSetRegistryResource.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2021 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.ibm.fhir.term.registry; - -import java.util.logging.Logger; - -import com.ibm.fhir.model.resource.Resource; -import com.ibm.fhir.model.resource.ValueSet; -import com.ibm.fhir.registry.resource.FHIRRegistryResource; - -public class ValueSetRegistryResource extends FHIRRegistryResource { - private static final Logger log = Logger.getLogger(ValueSetRegistryResource.class.getName()); - - protected final ValueSet valueSet; - - private ValueSetRegistryResource(String id, String url, Version version, ValueSet valueSet) { - super(ValueSet.class, id, url, version, null, null); - this.valueSet = valueSet; - } - - @Override - public Resource getResource() { - return valueSet; - } - - public static ValueSetRegistryResource from(ValueSet valueSet) { - String id = valueSet.getId(); - String url = (valueSet.getUrl() != null) ? valueSet.getUrl().getValue() : null; - String version = (valueSet.getVersion() != null) ? valueSet.getVersion().getValue() : null; - if (url == null) { - log.warning(String.format("Could not create ValueSetRegistryResource from ValueSet with: id: %s, url: %s, and version: %s", id, url, version)); - return null; - } - return new ValueSetRegistryResource(id, url, (version != null) ? Version.from(version) : FHIRRegistryResource.NO_VERSION, valueSet); - } -} diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/registry/resource/FHIRTermRegistryResource.java b/fhir-term/src/main/java/com/ibm/fhir/term/registry/resource/FHIRTermRegistryResource.java new file mode 100644 index 00000000000..289fba66e0d --- /dev/null +++ b/fhir-term/src/main/java/com/ibm/fhir/term/registry/resource/FHIRTermRegistryResource.java @@ -0,0 +1,56 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.registry.resource; + +import java.util.Objects; +import java.util.logging.Logger; + +import com.ibm.fhir.model.resource.CodeSystem; +import com.ibm.fhir.model.resource.ConceptMap; +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.util.FHIRRegistryUtil; + +/* + * A registry resource that contains a CodeSystem, ConceptMap, or ValueSet + */ +public class FHIRTermRegistryResource extends FHIRRegistryResource { + private static final Logger log = Logger.getLogger(FHIRTermRegistryResource.class.getName()); + + private final T resource; + + private FHIRTermRegistryResource(String id, String url, Version version, T resource) { + super(resource.getClass(), id, url, version, null, null); + this.resource = Objects.requireNonNull(resource, "resource"); + } + + @Override + public Resource getResource() { + return resource; + } + + public static FHIRRegistryResource from(T resource) { + Objects.requireNonNull(resource, "resource"); + + Class resourceType = resource.getClass(); + if (!CodeSystem.class.equals(resourceType) && !ConceptMap.class.equals(resourceType) && !ValueSet.class.equals(resourceType)) { + throw new IllegalArgumentException("Expected resource type: CodeSystem, ConceptMap, or ValueSet but found: " + resourceType.getClass().getSimpleName()); + } + + String id = resource.getId(); + String url = FHIRRegistryUtil.getUrl(resource); + String version = FHIRRegistryUtil.getVersion(resource); + + if (url == null) { + log.warning(String.format("Could not create FHIRTermRegistryResource from " + resource.getClass().getSimpleName() + " with: id: %s, url: %s, and version: %s", id, url, version)); + return null; + } + + return new FHIRTermRegistryResource(id, url, (version != null) ? Version.from(version) : FHIRRegistryResource.NO_VERSION, resource); + } +} diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java b/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java index cc1248ebcd9..13a1d8ef814 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java @@ -680,6 +680,7 @@ public ValidationOutcome validateCode(CodeSystem codeSystem, String version, Cod throw new UnsupportedOperationException("Validation parameters are not supported"); } Coding coding = Coding.builder() + .system(codeSystem.getUrl()) .version(version) .code(code) .display(display) From 4c1e7ce2e15b792ab58aa5ad81bf2471f8833788 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 16 Mar 2021 12:05:51 -0400 Subject: [PATCH 28/50] Issue #1980, Issue #2092 - removed System.out Signed-off-by: John T.E. Timm --- .../term/graph/registry/SnomedRegistryResourceProvider.java | 3 --- 1 file changed, 3 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 0348ec3bb9e..1d3acaedb77 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 @@ -65,9 +65,6 @@ public FHIRRegistryResource getRegistryResource(Class resour String sctid = tokens[1]; ValidationOutcome outcome = FHIRTermService.getInstance().validateCode(SNOMED_CODE_SYSTEM, null, Code.of(sctid), null); if (outcome == null || (Boolean.FALSE.equals(outcome.getResult()))) { - if (outcome != null) { - System.out.println("outcome: " + outcome.toParameters()); - } log.log(Level.WARNING, "Code: " + sctid + " is invalid or SNOMED CT is not supported"); return null; } From e11c1bf7535e8af531056181b7cd68df3a14b3a6 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 16 Mar 2021 12:53:30 -0400 Subject: [PATCH 29/50] Issue #1980 - fix issue in CodeSystem validate-code operation Signed-off-by: John T.E. Timm --- .../fhir/operation/term/CodeSystemValidateCodeOperation.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/CodeSystemValidateCodeOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/CodeSystemValidateCodeOperation.java index 6c804cee6de..cef7a9a8c7e 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/CodeSystemValidateCodeOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/CodeSystemValidateCodeOperation.java @@ -38,6 +38,11 @@ protected Parameters doInvoke( try { CodeSystem codeSystem = getResource(operationContext, logicalId, parameters, resourceHelper, CodeSystem.class); Element codedElement = getCodedElement(parameters, "codeableConcept", "coding", "code", false); + if (codedElement.is(Coding.class) && codedElement.as(Coding.class).getSystem() == null) { + codedElement = codedElement.as(Coding.class).toBuilder() + .system(codeSystem.getUrl()) + .build(); + } validate(codeSystem, codedElement); ValidationOutcome outcome = codedElement.is(CodeableConcept.class) ? service.validateCode(codeSystem, codedElement.as(CodeableConcept.class), ValidationParameters.from(parameters)) : From 21ffe8aac34a9b4d8f69bc0defd9cbbb2f5d24e8 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 16 Mar 2021 12:53:50 -0400 Subject: [PATCH 30/50] Issue #1980 - update copyright Signed-off-by: John T.E. Timm --- .../fhir/operation/term/CodeSystemValidateCodeOperation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/CodeSystemValidateCodeOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/CodeSystemValidateCodeOperation.java index cef7a9a8c7e..576b9630e1a 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/CodeSystemValidateCodeOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/CodeSystemValidateCodeOperation.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */ From 9b8db1126c28089cfc878ef7c858ddfafa2c36a3 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Wed, 17 Mar 2021 10:57:07 -0400 Subject: [PATCH 31/50] Issue #1980 - additional updates and clean-up Signed-off-by: John T.E. Timm --- .../resource/FHIRRegistryResource.java | 58 +++++++++++++++++- .../util/PackageRegistryResource.java | 4 +- .../registry/ServerRegistryResource.java | 61 ------------------- .../ServerRegistryResourceProvider.java | 6 +- .../provider/GraphTermServiceProvider.java | 3 + .../SnomedRegistryResourceProvider.java | 9 ++- .../resource/FHIRTermRegistryResource.java | 56 ----------------- .../fhir/term/service/FHIRTermService.java | 20 +++--- ....java => RegistryTermServiceProvider.java} | 4 +- 9 files changed, 76 insertions(+), 145 deletions(-) delete mode 100644 fhir-server/src/main/java/com/ibm/fhir/server/registry/ServerRegistryResource.java delete mode 100644 fhir-term/src/main/java/com/ibm/fhir/term/registry/resource/FHIRTermRegistryResource.java rename fhir-term/src/main/java/com/ibm/fhir/term/service/provider/{DefaultTermServiceProvider.java => RegistryTermServiceProvider.java} (90%) 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 79fddbf8760..d10462d74d1 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 @@ -6,14 +6,22 @@ package com.ibm.fhir.registry.resource; +import static com.ibm.fhir.registry.util.FHIRRegistryUtil.requireDefinitionalResourceType; + import java.util.Objects; +import java.util.logging.Logger; import com.ibm.fhir.model.resource.Resource; +import com.ibm.fhir.model.resource.SearchParameter; +import com.ibm.fhir.model.resource.StructureDefinition; +import com.ibm.fhir.registry.util.FHIRRegistryUtil; /** - * An abstract base class that contains the metadata for a definitional resource (e.g. StructureDefinition) + * A base class that contains the metadata for a definitional resource (e.g. StructureDefinition) */ -public abstract class FHIRRegistryResource implements Comparable { +public class FHIRRegistryResource implements Comparable { + private static final Logger log = Logger.getLogger(FHIRRegistryResource.class.getName()); + public static final Version NO_VERSION = Version.from(""); protected final Class resourceType; @@ -23,6 +31,8 @@ public abstract class FHIRRegistryResource implements Comparable resourceType, String id, @@ -38,6 +48,18 @@ public FHIRRegistryResource( this.type = type; } + public FHIRRegistryResource( + Class resourceType, + String id, + String url, + Version version, + String kind, + String type, + Resource resource) { + this(resourceType, id, url, version, kind, type); + this.resource = resource; + } + public Class getResourceType() { return resourceType; } @@ -62,7 +84,9 @@ public String getType() { return type; } - public abstract Resource getResource(); + public Resource getResource() { + return resource; + } public boolean is(Class registryResourceType) { return registryResourceType.isInstance(this); @@ -204,4 +228,32 @@ public int compareTo(Version version) { } } } + + public static FHIRRegistryResource from(Resource resource) { + Objects.requireNonNull(resource, "resource"); + + Class resourceType = resource.getClass(); + requireDefinitionalResourceType(resourceType); + + String id = resource.getId(); + String url = FHIRRegistryUtil.getUrl(resource); + String version = FHIRRegistryUtil.getVersion(resource); + if (url == null) { + log.warning(String.format("Could not create FHIRRegistryResource from Resource with resourceType: %s, id: %s, url: %s, and version: %s", resourceType.getSimpleName(), id, url, version)); + return null; + } + + String kind = null; + String type = null; + if (resource instanceof StructureDefinition) { + StructureDefinition structureDefinition = (StructureDefinition) resource; + kind = structureDefinition.getKind().getValue(); + type = structureDefinition.getType().getValue(); + } else if (resource instanceof SearchParameter) { + SearchParameter searchParameter = (SearchParameter) resource; + type = searchParameter.getType().getValue(); + } + + return new FHIRRegistryResource(resourceType, id, url, (version != null) ? Version.from(version) : FHIRRegistryResource.NO_VERSION, kind, type, resource); + } } \ No newline at end of file diff --git a/fhir-registry/src/main/java/com/ibm/fhir/registry/util/PackageRegistryResource.java b/fhir-registry/src/main/java/com/ibm/fhir/registry/util/PackageRegistryResource.java index 4dc49db5a4c..cee197f6737 100644 --- a/fhir-registry/src/main/java/com/ibm/fhir/registry/util/PackageRegistryResource.java +++ b/fhir-registry/src/main/java/com/ibm/fhir/registry/util/PackageRegistryResource.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */ @@ -19,8 +19,6 @@ public class PackageRegistryResource extends FHIRRegistryResource { protected final String path; - protected volatile Resource resource; - public PackageRegistryResource( Class resourceType, String id, diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/registry/ServerRegistryResource.java b/fhir-server/src/main/java/com/ibm/fhir/server/registry/ServerRegistryResource.java deleted file mode 100644 index a12048a9217..00000000000 --- a/fhir-server/src/main/java/com/ibm/fhir/server/registry/ServerRegistryResource.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2020, 2021 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.ibm.fhir.server.registry; - -import java.util.Objects; -import java.util.logging.Logger; - -import com.ibm.fhir.model.resource.Resource; -import com.ibm.fhir.model.resource.SearchParameter; -import com.ibm.fhir.model.resource.StructureDefinition; -import com.ibm.fhir.registry.resource.FHIRRegistryResource; -import com.ibm.fhir.registry.util.FHIRRegistryUtil; - -public class ServerRegistryResource extends FHIRRegistryResource { - private static final Logger log = Logger.getLogger(ServerRegistryResource.class.getName()); - - private final Resource resource; - - public ServerRegistryResource( - Class resourceType, - String id, - String url, - Version version, - String kind, - String type, - Resource resource) { - super(resourceType, id, url, version, kind, type); - this.resource = Objects.requireNonNull(resource); - } - - @Override - public Resource getResource() { - return resource; - } - - public static ServerRegistryResource from(Resource resource) { - Class resourceType = resource.getClass(); - String id = resource.getId(); - String url = FHIRRegistryUtil.getUrl(resource); - String version = FHIRRegistryUtil.getVersion(resource); - if (url == null) { - log.warning(String.format("Could not create ServerRegistryResource from Resource with resourceType: %s, id: %s, url: %s, and version: %s", resourceType.getSimpleName(), id, url, version)); - return null; - } - String kind = null; - String type = null; - if (resource instanceof StructureDefinition) { - StructureDefinition structureDefinition = (StructureDefinition) resource; - kind = structureDefinition.getKind().getValue(); - type = structureDefinition.getType().getValue(); - } else if (resource instanceof SearchParameter) { - SearchParameter searchParameter = (SearchParameter) resource; - type = searchParameter.getType().getValue(); - } - return new ServerRegistryResource(resourceType, id, url, (version != null) ? Version.from(version) : FHIRRegistryResource.NO_VERSION, kind, type, resource); - } -} diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/registry/ServerRegistryResourceProvider.java b/fhir-server/src/main/java/com/ibm/fhir/server/registry/ServerRegistryResourceProvider.java index 9b1ec003964..83f26fae464 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/registry/ServerRegistryResourceProvider.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/registry/ServerRegistryResourceProvider.java @@ -155,7 +155,7 @@ private List computeRegistryResources(Class getRegistryResources(Class registryResources = new ArrayList<>(searchContext.getTotalCount()); registryResources.addAll(result.getResource().stream() - .map(ServerRegistryResource::from) + .map(FHIRRegistryResource::from) .filter(Objects::nonNull) .collect(Collectors.toList())); @@ -201,7 +201,7 @@ private Collection getRegistryResources(Class SNOMED_SUBSUMED_BY_IMPLICIT_VALUE_SET_REGISTRY_RESOURCE_CACHE = createLRUCache(128); @Override @@ -63,12 +62,12 @@ public FHIRRegistryResource getRegistryResource(Class resour String[] tokens = url.split("="); if (tokens.length == 2) { String sctid = tokens[1]; - ValidationOutcome outcome = FHIRTermService.getInstance().validateCode(SNOMED_CODE_SYSTEM, null, Code.of(sctid), null); + 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 -> FHIRTermRegistryResource.from(buildSubsumedByImplicitValueSet(sctid))); + 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)) { diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/registry/resource/FHIRTermRegistryResource.java b/fhir-term/src/main/java/com/ibm/fhir/term/registry/resource/FHIRTermRegistryResource.java deleted file mode 100644 index 289fba66e0d..00000000000 --- a/fhir-term/src/main/java/com/ibm/fhir/term/registry/resource/FHIRTermRegistryResource.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * (C) Copyright IBM Corp. 2021 - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.ibm.fhir.term.registry.resource; - -import java.util.Objects; -import java.util.logging.Logger; - -import com.ibm.fhir.model.resource.CodeSystem; -import com.ibm.fhir.model.resource.ConceptMap; -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.util.FHIRRegistryUtil; - -/* - * A registry resource that contains a CodeSystem, ConceptMap, or ValueSet - */ -public class FHIRTermRegistryResource extends FHIRRegistryResource { - private static final Logger log = Logger.getLogger(FHIRTermRegistryResource.class.getName()); - - private final T resource; - - private FHIRTermRegistryResource(String id, String url, Version version, T resource) { - super(resource.getClass(), id, url, version, null, null); - this.resource = Objects.requireNonNull(resource, "resource"); - } - - @Override - public Resource getResource() { - return resource; - } - - public static FHIRRegistryResource from(T resource) { - Objects.requireNonNull(resource, "resource"); - - Class resourceType = resource.getClass(); - if (!CodeSystem.class.equals(resourceType) && !ConceptMap.class.equals(resourceType) && !ValueSet.class.equals(resourceType)) { - throw new IllegalArgumentException("Expected resource type: CodeSystem, ConceptMap, or ValueSet but found: " + resourceType.getClass().getSimpleName()); - } - - String id = resource.getId(); - String url = FHIRRegistryUtil.getUrl(resource); - String version = FHIRRegistryUtil.getVersion(resource); - - if (url == null) { - log.warning(String.format("Could not create FHIRTermRegistryResource from " + resource.getClass().getSimpleName() + " with: id: %s, url: %s, and version: %s", id, url, version)); - return null; - } - - return new FHIRTermRegistryResource(id, url, (version != null) ? Version.from(version) : FHIRRegistryResource.NO_VERSION, resource); - } -} diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java b/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java index 13a1d8ef814..aec8d702603 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java @@ -35,7 +35,7 @@ import com.ibm.fhir.model.type.Uri; import com.ibm.fhir.model.type.code.CodeSystemHierarchyMeaning; import com.ibm.fhir.model.type.code.ConceptSubsumptionOutcome; -import com.ibm.fhir.term.service.provider.DefaultTermServiceProvider; +import com.ibm.fhir.term.service.provider.RegistryTermServiceProvider; import com.ibm.fhir.term.spi.ExpansionParameters; import com.ibm.fhir.term.spi.FHIRTermServiceProvider; import com.ibm.fhir.term.spi.LookupOutcome; @@ -642,12 +642,10 @@ public ValidationOutcome validateCode(CodeSystem codeSystem, Coding coding, Vali } /** - * Validate a code and display using the provided code system and version + * Validate a code and display using the provided code system * * @param code system * the code system - * @param version - * the version * @param code * the code * @param display @@ -655,17 +653,15 @@ public ValidationOutcome validateCode(CodeSystem codeSystem, Coding coding, Vali * @return * the outcome of validation */ - public ValidationOutcome validateCode(CodeSystem codeSystem, String version, Code code, String display) { - return validateCode(codeSystem, version, code, display, ValidationParameters.EMPTY); + public ValidationOutcome validateCode(CodeSystem codeSystem, Code code, String display) { + return validateCode(codeSystem, code, display, ValidationParameters.EMPTY); } /** - * Validate a code and display using the provided code system, version and validation parameters + * Validate a code and display using the provided code system and validation parameters * * @param codeSystem * the code system - * @param version - * the version * @param code * the code * @param display @@ -675,13 +671,13 @@ public ValidationOutcome validateCode(CodeSystem codeSystem, String version, Cod * @return * the outcome of validation */ - public ValidationOutcome validateCode(CodeSystem codeSystem, String version, Code code, String display, ValidationParameters parameters) { + public ValidationOutcome validateCode(CodeSystem codeSystem, Code code, String display, ValidationParameters parameters) { if (!ValidationParameters.EMPTY.equals(parameters)) { throw new UnsupportedOperationException("Validation parameters are not supported"); } Coding coding = Coding.builder() .system(codeSystem.getUrl()) - .version(version) + .version(codeSystem.getVersion()) .code(code) .display(display) .build(); @@ -884,7 +880,7 @@ private Uri getSource(ConceptMap conceptMap) { private List loadProviders() { List providers = new ArrayList<>(); - providers.add(new DefaultTermServiceProvider()); + providers.add(new RegistryTermServiceProvider()); Iterator iterator = ServiceLoader.load(FHIRTermServiceProvider.class).iterator(); while (iterator.hasNext()) { providers.add(iterator.next()); diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java b/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/RegistryTermServiceProvider.java similarity index 90% rename from fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java rename to fhir-term/src/main/java/com/ibm/fhir/term/service/provider/RegistryTermServiceProvider.java index a1622b487e8..cdfdb47b8bb 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/DefaultTermServiceProvider.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/service/provider/RegistryTermServiceProvider.java @@ -18,9 +18,9 @@ import com.ibm.fhir.term.util.CodeSystemSupport; /** - * Default implementation of the FHIRTermServiceProvider interface using CodeSystemSupport + * Registry-based implementation of the {@link FHIRTermServiceProvider} interface using {@link CodeSystemSupport} */ -public class DefaultTermServiceProvider implements FHIRTermServiceProvider { +public class RegistryTermServiceProvider implements FHIRTermServiceProvider { @Override public Set closure(CodeSystem codeSystem, Code code) { Concept concept = CodeSystemSupport.findConcept(codeSystem, code); From d1248c53cef1b497571a44fd59d07bbd90e659b1 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Wed, 17 Mar 2021 11:41:51 -0400 Subject: [PATCH 32/50] Issue #1980 - moved FHIRTermGraphUtil.normalize to CodeSystemSupport Signed-off-by: John T.E. Timm --- .../impl/CodeSystemTermGraphLoader.java | 2 +- .../loader/impl/SnomedTermGraphLoader.java | 2 +- .../loader/impl/UMLSTermGraphLoader.java | 2 +- .../provider/GraphTermServiceProvider.java | 8 ++++---- .../term/graph/util/FHIRTermGraphUtil.java | 17 ----------------- .../fhir/term/service/FHIRTermService.java | 3 ++- .../ibm/fhir/term/util/CodeSystemSupport.java | 19 ++++++++++++++++++- 7 files changed, 27 insertions(+), 26 deletions(-) diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java index f0ed7d2de00..f552a9ce125 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/CodeSystemTermGraphLoader.java @@ -7,10 +7,10 @@ package com.ibm.fhir.term.graph.loader.impl; import static com.ibm.fhir.term.graph.loader.util.FHIRTermGraphLoaderUtil.toMap; -import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toLong; import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toObject; import static com.ibm.fhir.term.util.CodeSystemSupport.getConcepts; +import static com.ibm.fhir.term.util.CodeSystemSupport.normalize; import java.io.FileInputStream; import java.io.InputStream; diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/SnomedTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/SnomedTermGraphLoader.java index 8953f0e7012..057b028787c 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/SnomedTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/SnomedTermGraphLoader.java @@ -8,7 +8,7 @@ import static com.ibm.fhir.term.graph.loader.util.FHIRTermGraphLoaderUtil.toLabel; import static com.ibm.fhir.term.graph.loader.util.FHIRTermGraphLoaderUtil.toMap; -import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; +import static com.ibm.fhir.term.util.CodeSystemSupport.normalize; import java.io.BufferedReader; import java.io.FileNotFoundException; diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/UMLSTermGraphLoader.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/UMLSTermGraphLoader.java index e67e385e58c..7ed4a1b74a9 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/UMLSTermGraphLoader.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/loader/impl/UMLSTermGraphLoader.java @@ -8,7 +8,7 @@ import static com.ibm.fhir.term.graph.loader.util.FHIRTermGraphLoaderUtil.toLabel; import static com.ibm.fhir.term.graph.loader.util.FHIRTermGraphLoaderUtil.toMap; -import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; +import static com.ibm.fhir.term.util.CodeSystemSupport.normalize; import java.io.BufferedReader; import java.io.FileNotFoundException; 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 7e2ca3ccc85..5e6018ba23b 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 @@ -7,13 +7,13 @@ package com.ibm.fhir.term.graph.provider; import static com.ibm.fhir.model.type.String.string; -import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.normalize; import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toLong; import static com.ibm.fhir.term.graph.util.FHIRTermGraphUtil.toObject; import static com.ibm.fhir.term.util.CodeSystemSupport.convertsToBoolean; import static com.ibm.fhir.term.util.CodeSystemSupport.getCodeSystemPropertyType; import static com.ibm.fhir.term.util.CodeSystemSupport.hasCodeSystemProperty; import static com.ibm.fhir.term.util.CodeSystemSupport.isCaseSensitive; +import static com.ibm.fhir.term.util.CodeSystemSupport.normalize; import static com.ibm.fhir.term.util.CodeSystemSupport.toElement; import java.util.ArrayList; @@ -54,9 +54,9 @@ import com.ibm.fhir.model.type.code.PropertyType; import com.ibm.fhir.term.graph.FHIRTermGraph; import com.ibm.fhir.term.graph.factory.FHIRTermGraphFactory; -import com.ibm.fhir.term.graph.util.FHIRTermGraphUtil; import com.ibm.fhir.term.service.exception.FHIRTermServiceException; import com.ibm.fhir.term.spi.FHIRTermServiceProvider; +import com.ibm.fhir.term.util.CodeSystemSupport; /** * Graph-based implementation of the {@link FHIRTermServiceProvider} interface using {@link FHIRTermGraph} @@ -204,7 +204,7 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { .collect(Collectors.toSet()))), codeSystem); } else { g = whereCodeSystem(g.has("codeLowerCase", P.within(Arrays.stream(value.getValue().split(",")) - .map(FHIRTermGraphUtil::normalize) + .map(CodeSystemSupport::normalize) .collect(Collectors.toSet()))), codeSystem); } } else { @@ -242,7 +242,7 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { .collect(Collectors.toSet()))), codeSystem); } else { g = whereCodeSystem(g.has("codeLowerCase", P.without(Arrays.stream(value.getValue().split(",")) - .map(FHIRTermGraphUtil::normalize) + .map(CodeSystemSupport::normalize) .collect(Collectors.toSet()))), codeSystem); } } else { diff --git a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java index 5275111dd5d..de2deb8a1aa 100644 --- a/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java +++ b/fhir-term-graph/src/main/java/com/ibm/fhir/term/graph/util/FHIRTermGraphUtil.java @@ -10,8 +10,6 @@ import static com.ibm.fhir.model.util.ModelSupport.FHIR_INTEGER; import static com.ibm.fhir.model.util.ModelSupport.FHIR_STRING; -import java.text.Normalizer; -import java.text.Normalizer.Form; import java.time.LocalDate; import java.time.Year; import java.time.YearMonth; @@ -62,21 +60,6 @@ public static Object toObject(Element value) { throw new IllegalArgumentException(); } - /** - * Normalize the string by making it case and accent insensitive. - * - * @param value - * the string value to normalized - * @return - * the normalized string value - */ - public static String normalize(String value) { - if (value != null) { - return Normalizer.normalize(value, Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "").toLowerCase(); - } - return null; - } - /** * Sets the root logger level for the logback classic root logger. See: * JanusGraph common questions diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java b/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java index aec8d702603..889afe3bd48 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/service/FHIRTermService.java @@ -8,6 +8,7 @@ import static com.ibm.fhir.model.type.String.string; import static com.ibm.fhir.model.util.FHIRUtil.STRING_DATA_ABSENT_REASON_UNKNOWN; +import static com.ibm.fhir.term.util.CodeSystemSupport.normalize; import java.util.ArrayList; import java.util.Collections; @@ -906,7 +907,7 @@ private ValidationOutcome validateCode(CodeSystem codeSystem, Coding coding, boo java.lang.String url = (version != null) ? system + "|" + version : system; caseSensitive = CodeSystemSupport.isCaseSensitive(url); } - result = caseSensitive ? outcome.getDisplay().equals(coding.getDisplay()) : outcome.getDisplay().getValue().equalsIgnoreCase(coding.getDisplay().getValue()); + result = caseSensitive ? outcome.getDisplay().equals(coding.getDisplay()) : normalize(outcome.getDisplay().getValue()).equals(normalize(coding.getDisplay().getValue())); message = !result ? java.lang.String.format("The display '%s' is incorrect for code '%s' from code system '%s'", coding.getDisplay().getValue(), coding.getCode().getValue(), system) : null; } return ValidationOutcome.builder() 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 65eb0fb82e7..22c445b970f 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 @@ -9,6 +9,8 @@ import static com.ibm.fhir.core.util.LRUCache.createLRUCache; import static com.ibm.fhir.model.type.String.string; +import java.text.Normalizer; +import java.text.Normalizer.Form; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -92,7 +94,7 @@ public static Concept findConcept(CodeSystem codeSystem, Code code) { * the code system concept that matches the specified code, or null if not such concept exists */ public static Concept findConcept(CodeSystem codeSystem, Concept concept, Code code) { - if (concept.getCode().equals(code) || (!isCaseSensitive(codeSystem)) && concept.getCode().getValue().equalsIgnoreCase(code.getValue())) { + if (concept.getCode().equals(code) || (!isCaseSensitive(codeSystem)) && normalize(concept.getCode().getValue()).equals(normalize(code.getValue()))) { return concept; } Concept result = null; @@ -343,6 +345,21 @@ public static Set getConcepts(Concept concept) { return concepts; } + /** + * Normalize the string by making it case and accent insensitive. + * + * @param value + * the string value to normalized + * @return + * the normalized string value + */ + public static java.lang.String normalize(java.lang.String value) { + if (value != null) { + return Normalizer.normalize(value, Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "").toLowerCase(); + } + return null; + } + /** * Convert the given FHIR string value to a FHIR boolean value. * From 965b878175645a77e372446d9e5273420e9176c3 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Wed, 17 Mar 2021 12:15:49 -0400 Subject: [PATCH 33/50] Issue #1980 - removed System.out Signed-off-by: John T.E. Timm --- .../ibm/fhir/term/graph/provider/GraphTermServiceProvider.java | 1 - 1 file changed, 1 deletion(-) 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 5e6018ba23b..bbd4b58283b 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 @@ -457,7 +457,6 @@ private Element getValue(Map elementMap) { if (elementMap.containsKey("valueString")) { return string((String) elementMap.get("valueString")); } - System.out.println("elementMap: " + elementMap); return null; } From a0f9c0823e65a8dba78df41cee2ddf2cd7bfd651 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Wed, 17 Mar 2021 14:40:11 -0400 Subject: [PATCH 34/50] Issue #1980 - updated ValueSet support to use normalize Signed-off-by: John T.E. Timm --- .../main/java/com/ibm/fhir/term/util/ValueSetSupport.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java b/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java index 15143bcb180..841d85f65b8 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java @@ -10,6 +10,7 @@ import static com.ibm.fhir.model.type.String.string; import static com.ibm.fhir.term.util.CodeSystemSupport.getCodeSystem; import static com.ibm.fhir.term.util.CodeSystemSupport.isCaseSensitive; +import static com.ibm.fhir.term.util.CodeSystemSupport.normalize; import java.time.ZoneOffset; import java.util.ArrayList; @@ -401,7 +402,7 @@ private static boolean validateCode(Map> } java.lang.String url = (version != null) ? system + "|" + version : system; if (!isCaseSensitive(url)) { - code = code.toLowerCase(); + code = normalize(code); } if (version != null) { Set codeSet = codeSetMap.get(system + "|" + version); @@ -449,7 +450,7 @@ private static Map> computeCodeSetMap(Va if (system != null && code != null) { java.lang.String url = !VERSION_UNKNOWN.equals(version) ? system + "|" + version : system; if (!isCaseSensitive(url)) { - code = code.toLowerCase(); + code = normalize(code); } codeSetMap.computeIfAbsent(system + "|" + version, k -> new LinkedHashSet<>()).add(code); } From 7682d62c88adfb913cbf6b4c1ab390fee3dc807b Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Thu, 18 Mar 2021 09:53:29 -0400 Subject: [PATCH 35/50] Issue #1980 - updates Signed-off-by: John T.E. Timm --- .../operation/spi/AbstractOperation.java | 108 ++++++++---------- 1 file changed, 49 insertions(+), 59 deletions(-) diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/operation/spi/AbstractOperation.java b/fhir-server/src/main/java/com/ibm/fhir/server/operation/spi/AbstractOperation.java index d319b93dd4c..e1817e7dfb9 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/operation/spi/AbstractOperation.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/operation/spi/AbstractOperation.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2016,2019 + * (C) Copyright IBM Corp. 2016, 2021 * * SPDX-License-Identifier: Apache-2.0 */ @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import com.ibm.fhir.exception.FHIROperationException; import com.ibm.fhir.model.resource.OperationDefinition; @@ -16,15 +17,15 @@ import com.ibm.fhir.model.resource.Resource; import com.ibm.fhir.model.type.code.IssueType; import com.ibm.fhir.model.type.code.OperationParameterUse; +import com.ibm.fhir.model.type.code.ResourceType; import com.ibm.fhir.model.util.FHIRUtil; import com.ibm.fhir.model.util.ModelSupport; public abstract class AbstractOperation implements FHIROperation { - - protected OperationDefinition definition = null; + protected final OperationDefinition definition; public AbstractOperation() { - definition = buildOperationDefinition(); + definition = Objects.requireNonNull(buildOperationDefinition(), "definition"); } @Override @@ -34,21 +35,22 @@ public OperationDefinition getDefinition() { @Override public String getName() { - if (definition.getCode() == null) - return null; - else - return definition.getCode().getValue(); + return (definition.getCode() != null) ? definition.getCode().getValue() : null; } /** * Validate the input parameters, invoke the operation, validate the output parameters, and return the result. * - * @throws FHIROperationException if input or output parameters fail validation or an exception occurs + * @throws FHIROperationException + * if input or output parameters fail validation or an exception occurs */ @Override - public final Parameters invoke(FHIROperationContext operationContext, Class resourceType, - String logicalId, String versionId, Parameters parameters, FHIRResourceHelpers resourceHelper) - throws FHIROperationException { + public final Parameters invoke( + FHIROperationContext operationContext, + Class resourceType, + String logicalId, String versionId, + Parameters parameters, + FHIRResourceHelpers resourceHelper) throws FHIROperationException { validateOperationContext(operationContext, resourceType); validateInputParameters(operationContext, resourceType, logicalId, versionId, parameters); Parameters result = doInvoke(operationContext, resourceType, logicalId, versionId, parameters, resourceHelper); @@ -65,11 +67,15 @@ protected int countParameters(Parameters parameters, String name) { /** * This is the method that concrete subclasses must implement to perform the operation logic. * - * @return The Parameters object to return or null if there is no response Parameters object to return + * @return + * the Parameters object to return or null if there is no response Parameters object to return * @throws FHIROperationException */ - protected abstract Parameters doInvoke(FHIROperationContext operationContext, - Class resourceType, String logicalId, String versionId, Parameters parameters, + protected abstract Parameters doInvoke( + FHIROperationContext operationContext, + Class resourceType, + String logicalId, String versionId, + Parameters parameters, FHIRResourceHelpers resourceHelper) throws FHIROperationException; protected Parameters.Parameter getParameter(Parameters parameters, String name) { @@ -97,7 +103,6 @@ protected List getParameters(Parameters parameters, String if (parameters == null) { return result; } - for (Parameters.Parameter parameter : parameters.getParameter()) { if (parameter.getName() != null && name.equals(parameter.getName().getValue())) { result.add(parameter); @@ -109,20 +114,22 @@ protected List getParameters(Parameters parameters, String protected List getResourceTypeNames() { List resourceTypeNames = new ArrayList(); OperationDefinition definition = getDefinition(); - for (com.ibm.fhir.model.type.code.ResourceType type : definition.getResource()) { + for (ResourceType type : definition.getResource()) { resourceTypeNames.add(type.getValue()); } return resourceTypeNames; } - protected void validateInputParameters(FHIROperationContext operationContext, - Class resourceType, String logicalId, String versionId, Parameters parameters) - throws FHIROperationException { + protected void validateInputParameters( + FHIROperationContext operationContext, + Class resourceType, + String logicalId, + String versionId, + Parameters parameters) throws FHIROperationException { validateParameters(parameters, OperationParameterUse.IN); } - protected void validateOperationContext(FHIROperationContext operationContext, - Class resourceType) throws FHIROperationException { + protected void validateOperationContext(FHIROperationContext operationContext, Class resourceType) throws FHIROperationException { OperationDefinition definition = getDefinition(); switch (operationContext.getType()) { case INSTANCE: @@ -139,9 +146,7 @@ protected void validateOperationContext(FHIROperationContext operationContext, String resourceTypeName = resourceType.getSimpleName(); List resourceTypeNames = getResourceTypeNames(); if (!resourceTypeNames.contains(resourceTypeName) && !resourceTypeNames.contains("Resource")) { - String msg = - "Resource type: '" + resourceTypeName + "' is not allowed for operation: '" + getName() - + "'"; + String msg = "Resource type: '" + resourceTypeName + "' is not allowed for operation: '" + getName() + "'"; throw buildExceptionWithIssue(msg, IssueType.INVALID); } } @@ -180,9 +185,7 @@ protected void validateParameters(Parameters parameters, OperationParameterUse u if (!"*".equals(max)) { int maxValue = Integer.parseInt(max); if (count > maxValue) { - String msg = - "Number of occurrences of " + direction + " parameter: '" + name - + "' greater than allowed maximum: " + maxValue; + String msg = "Number of occurrences of " + direction + " parameter: '" + name + "' greater than allowed maximum: " + maxValue; throw buildExceptionWithIssue(msg, IssueType.INVALID); } } @@ -191,29 +194,23 @@ protected void validateParameters(Parameters parameters, OperationParameterUse u for (Parameters.Parameter inputParameter : inputParameters) { // Check to see if it's a parameter if (inputParameter.getPart() == null || inputParameter.getPart().isEmpty()) { - String parameterValueTypeName = - inputParameter.getResource() != null ? inputParameter.getResource().getClass().getName() - : inputParameter.getValue().getClass().getName(); + String parameterValueTypeName = (inputParameter.getResource() != null) ? + inputParameter.getResource().getClass().getName() : + inputParameter.getValue().getClass().getName(); String parameterDefinitionTypeName = parameterDefinition.getType().getValue(); - parameterDefinitionTypeName = - parameterDefinitionTypeName.substring(0, 1).toUpperCase() - + parameterDefinitionTypeName.substring(1); + parameterDefinitionTypeName = parameterDefinitionTypeName.substring(0, 1).toUpperCase() + parameterDefinitionTypeName.substring(1); try { Class parameterValueType, parameterDefinitionType; parameterValueType = Class.forName(parameterValueTypeName); if (ModelSupport.isResourceType(parameterDefinitionTypeName)) { - parameterDefinitionType = - Class.forName("com.ibm.fhir.model.resource." + parameterDefinitionTypeName); + parameterDefinitionType = Class.forName("com.ibm.fhir.model.resource." + parameterDefinitionTypeName); } else { - parameterDefinitionType = - Class.forName("com.ibm.fhir.model.type." + parameterDefinitionTypeName); + parameterDefinitionType = Class.forName("com.ibm.fhir.model.type." + parameterDefinitionTypeName); } if (!parameterDefinitionType.isAssignableFrom(parameterValueType)) { - String msg = - "Invalid type: '" + parameterValueTypeName + "' for " + direction - + " parameter: '" + name + "'"; + String msg = "Invalid type: '" + parameterValueTypeName + "' for " + direction + " parameter: '" + name + "'"; throw buildExceptionWithIssue(msg, IssueType.INVALID); } } catch (FHIROperationException e) { @@ -221,23 +218,16 @@ protected void validateParameters(Parameters parameters, OperationParameterUse u } catch (Exception e) { throw new FHIROperationException("An unexpected error occurred during type checking", e); } - /* - * if (!parameterValueTypeName.equalsIgnoreCase(parameterDefinition.getType().getValue())) { - * throw new FHIROperationException("Invalid type: '" + parameterValueTypeName + "' for " + - * direction + " parameter: '" + name + "'"); - * } - */ } } } } - // Next, if parameters is not null, verify that each parameter contained in the Parameters object is defined - // in the OperationDefinition. This will root out any extaneous parameters included - // in the Parameters object. if (parameters == null) { return; } + + // Verify that each parameter contained in the Parameters object is defined in the OperationDefinition. for (Parameters.Parameter p : parameters.getParameter()) { String name = p.getName().getValue(); @@ -253,14 +243,16 @@ protected void validateParameters(Parameters parameters, OperationParameterUse u } /** - * Returns the OperationDefinitionParameter with the specified name or null if it wasn't found. + * Find the operation definition parameter with the specified name. * - * @param parameters the list of parameters from the OperationDefinition - * @param name the name of the parameter to find + * @param parameters + * the list of parameters from the OperationDefinition + * @param name + * the name of the parameter to find * @return + * the operation definition parameter with the specified name or null if not found */ - protected OperationDefinition.Parameter findOpDefParameter(List parameters, - String name) { + protected OperationDefinition.Parameter findOpDefParameter(List parameters, String name) { for (OperationDefinition.Parameter p : parameters) { if (p.getName().getValue().equals(name)) { return p; @@ -269,13 +261,11 @@ protected OperationDefinition.Parameter findOpDefParameter(List Date: Thu, 18 Mar 2021 14:17:39 -0400 Subject: [PATCH 36/50] Issue #1980 - updates Signed-off-by: John T.E. Timm --- .../com/ibm/fhir/registry/FHIRRegistry.java | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/fhir-registry/src/main/java/com/ibm/fhir/registry/FHIRRegistry.java b/fhir-registry/src/main/java/com/ibm/fhir/registry/FHIRRegistry.java index 55eea9cb26b..a4263852593 100644 --- a/fhir-registry/src/main/java/com/ibm/fhir/registry/FHIRRegistry.java +++ b/fhir-registry/src/main/java/com/ibm/fhir/registry/FHIRRegistry.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2019, 2020 + * (C) Copyright IBM Corp. 2019, 2021 * * SPDX-License-Identifier: Apache-2.0 */ @@ -13,7 +13,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -276,29 +276,18 @@ private List loadProviders() { } /** - * Given the list of providers, the method scans through the list to find all profile resource, and merge them together - * in order to develop a list of resource specific canonical URLs. + * Get a map containing sets of type specific canonical URLs for all profile resources from all providers. + * * @return + * the map of sets */ - public Map> getProfiles() { - Map> resourceTypeWithCanonicalUrls = new HashMap<>(); - providers.stream().map(provider -> provider.getProfileResources()) - .flatMap(Collection::stream) - .forEach(r -> processResource(r, resourceTypeWithCanonicalUrls)); - return resourceTypeWithCanonicalUrls; - } - - private void processResource(FHIRRegistryResource registryResource, Map> resourceTypeWithCanonicalUrls) { - String type = registryResource.getType(); - resourceTypeWithCanonicalUrls.compute(type, (k,v) -> checkOrCreateSet(k,v,registryResource)); - } - - private Set checkOrCreateSet(String k, Set v, FHIRRegistryResource registryResource) { - Canonical canonicalUrl = Canonical.of(registryResource.getUrl(), registryResource.getVersion().toString()); - if (v == null) { - v = new HashSet<>(); - } - v.add(canonicalUrl); - return v; + public Map> getProfiles() { + Map> map = new HashMap<>(); + providers.stream() + .map(provider -> provider.getProfileResources()) + .flatMap(Collection::stream) + .forEach(r -> map.computeIfAbsent(r.getType(), k -> new LinkedHashSet<>()) + .add(Canonical.of(r.getUrl(), r.getVersion().toString()))); + return map; } } \ No newline at end of file From b7b48408b2ca1692a2f173a3318675fda505f212 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Fri, 19 Mar 2021 09:29:11 -0400 Subject: [PATCH 37/50] Issue #1980 - performance improvements Signed-off-by: John T.E. Timm --- .../com/ibm/fhir/registry/FHIRRegistry.java | 87 +++++++++++-------- .../test/FHIRRegistryLatestVersionTest.java | 6 +- .../listener/FHIRServletContextListener.java | 2 +- .../test/InteractionValidationConfigTest.java | 2 +- .../test/ProfileValidationConfigTest.java | 2 +- .../ibm/fhir/term/util/CodeSystemSupport.java | 7 +- .../ibm/fhir/term/util/ValueSetSupport.java | 70 ++++++++------- .../test/ExpansionPerformanceTest.java | 30 +++++++ 8 files changed, 132 insertions(+), 74 deletions(-) create mode 100644 fhir-term/src/test/java/com/ibm/fhir/term/service/test/ExpansionPerformanceTest.java diff --git a/fhir-registry/src/main/java/com/ibm/fhir/registry/FHIRRegistry.java b/fhir-registry/src/main/java/com/ibm/fhir/registry/FHIRRegistry.java index a4263852593..1c6d139ae7f 100644 --- a/fhir-registry/src/main/java/com/ibm/fhir/registry/FHIRRegistry.java +++ b/fhir-registry/src/main/java/com/ibm/fhir/registry/FHIRRegistry.java @@ -13,6 +13,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -21,7 +22,6 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Logger; -import java.util.stream.Collectors; import com.ibm.fhir.model.resource.DomainResource; import com.ibm.fhir.model.resource.Resource; @@ -68,7 +68,7 @@ public static FHIRRegistry getInstance() { * @param provider * the registry resource provider to be added */ - public void register(FHIRRegistryResourceProvider provider) { + public void addProvider(FHIRRegistryResourceProvider provider) { Objects.requireNonNull(provider); providers.add(provider); } @@ -179,11 +179,13 @@ public T getResource(String url, Class resourceType) { public Collection getResources(Class resourceType) { Objects.requireNonNull(resourceType); requireDefinitionalResourceType(resourceType); - return providers.stream() - .map(provider -> provider.getRegistryResources(resourceType)) - .flatMap(Collection::stream) - .map(registryResource -> resourceType.cast(registryResource.getResource())) - .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + List resources = new ArrayList<>(); + for (FHIRRegistryResourceProvider provider : providers) { + for (FHIRRegistryResource registryResource : provider.getRegistryResources(resourceType)) { + resources.add(resourceType.cast(registryResource.getResource())); + } + } + return Collections.unmodifiableList(resources); } /** @@ -199,11 +201,16 @@ public Collection getProfiles(String type) { if (!ModelSupport.isResourceType(type)) { throw new IllegalArgumentException("The type argument must be a valid FHIR resource type name"); } - return providers.stream().map(provider -> provider.getProfileResources(type)) - .flatMap(Collection::stream) - .sorted() - .map(registryResource -> Canonical.of(registryResource.getUrl(), registryResource.getVersion().toString())) - .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + List registryResources = new ArrayList<>(); + for (FHIRRegistryResourceProvider provider : providers) { + registryResources.addAll(provider.getProfileResources(type)); + } + Collections.sort(registryResources); + List profiles = new ArrayList<>(); + for (FHIRRegistryResource registryResource : registryResources) { + profiles.add(Canonical.of(registryResource.getUrl(), registryResource.getVersion().toString())); + } + return Collections.unmodifiableList(profiles); } /** @@ -219,31 +226,38 @@ public Collection getProfiles(String type) { public Collection getSearchParameters(String type) { Objects.requireNonNull(type); SearchParamType.ValueSet.from(type); - return providers.stream() - .map(provider -> provider.getSearchParameterResources(type)) - .flatMap(Collection::stream) - .map(registryResource -> registryResource.getResource().as(SearchParameter.class)) - .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)); + List searchParameters = new ArrayList<>(); + for (FHIRRegistryResourceProvider provider : providers) { + for (FHIRRegistryResource registryResource : provider.getSearchParameterResources(type)) { + searchParameters.add(registryResource.getResource().as(SearchParameter.class)); + } + } + return Collections.unmodifiableList(searchParameters); + } private FHIRRegistryResource findRegistryResource(Class resourceType, String url, String version) { if (version == null) { // find the latest version of the registry resource with the specified resourceType and url (across all providers) - List registryResources = providers.stream() - .map(provider -> provider.getRegistryResource(resourceType, url, version)) - .filter(Objects::nonNull) - .distinct() - .sorted() - .collect(Collectors.toList()); + Set distinct = new HashSet<>(); + for (FHIRRegistryResourceProvider provider : providers) { + FHIRRegistryResource registryResource = provider.getRegistryResource(resourceType, url, version); + if (registryResource != null) { + distinct.add(registryResource); + } + } + List registryResources = new ArrayList<>(distinct); + Collections.sort(registryResources); return !registryResources.isEmpty() ? registryResources.get(registryResources.size() - 1) : null; } - // find the first registry resource with the specified resourceType, url, and version - return providers.stream() - .map(provider -> provider.getRegistryResource(resourceType, url, version)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); + for (FHIRRegistryResourceProvider provider : providers) { + FHIRRegistryResource registryResource = provider.getRegistryResource(resourceType, url, version); + if (registryResource != null) { + return registryResource; + } + } + return null; } private Resource getResource(FHIRRegistryResource registryResource, String url, String id) { @@ -276,18 +290,19 @@ private List loadProviders() { } /** - * Get a map containing sets of type specific canonical URLs for all profile resources from all providers. + * Get a map containing sets of type specific canonical URLs for all profile resources across all providers. * * @return * the map of sets */ public Map> getProfiles() { Map> map = new HashMap<>(); - providers.stream() - .map(provider -> provider.getProfileResources()) - .flatMap(Collection::stream) - .forEach(r -> map.computeIfAbsent(r.getType(), k -> new LinkedHashSet<>()) - .add(Canonical.of(r.getUrl(), r.getVersion().toString()))); + for (FHIRRegistryResourceProvider provider : providers) { + for (FHIRRegistryResource r : provider.getProfileResources()) { + map.computeIfAbsent(r.getType(), k -> new LinkedHashSet<>()) + .add(Canonical.of(r.getUrl(), r.getVersion().toString())); + } + } return map; } -} \ No newline at end of file +} diff --git a/fhir-registry/src/test/java/com/ibm/fhir/registry/test/FHIRRegistryLatestVersionTest.java b/fhir-registry/src/test/java/com/ibm/fhir/registry/test/FHIRRegistryLatestVersionTest.java index b9c8f89b89a..d199a3d5361 100644 --- a/fhir-registry/src/test/java/com/ibm/fhir/registry/test/FHIRRegistryLatestVersionTest.java +++ b/fhir-registry/src/test/java/com/ibm/fhir/registry/test/FHIRRegistryLatestVersionTest.java @@ -31,17 +31,17 @@ public class FHIRRegistryLatestVersionTest { static { - FHIRRegistry.getInstance().register( + FHIRRegistry.getInstance().addProvider( createRegistryResourceProvider( createRegistryResource(createStructureDefinition("1.0.0")), createRegistryResource(createStructureDefinition("2.0.0")), createRegistryResource(createStructureDefinition("3.0.0")))); - FHIRRegistry.getInstance().register( + FHIRRegistry.getInstance().addProvider( createRegistryResourceProvider( createRegistryResource(createStructureDefinition("4.0.0")), createRegistryResource(createStructureDefinition("5.0.0")), createRegistryResource(createStructureDefinition("6.0.0")))); - FHIRRegistry.getInstance().register( + FHIRRegistry.getInstance().addProvider( createRegistryResourceProvider( createRegistryResource(createStructureDefinition("7.0.0")), createRegistryResource(createStructureDefinition("8.0.0")), diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java b/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java index 2163a7ba337..35d0ca50a8c 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/listener/FHIRServletContextListener.java @@ -192,7 +192,7 @@ public void contextInitialized(ServletContextEvent event) { if (serverRegistryResourceProviderEnabled) { log.info("Registering ServerRegistryResourceProvider..."); ServerRegistryResourceProvider provider = new ServerRegistryResourceProvider(persistenceHelper); - FHIRRegistry.getInstance().register(provider); + FHIRRegistry.getInstance().addProvider(provider); FHIRPersistenceInterceptorMgr.getInstance().addInterceptor(provider); } diff --git a/fhir-server/src/test/java/com/ibm/fhir/server/test/InteractionValidationConfigTest.java b/fhir-server/src/test/java/com/ibm/fhir/server/test/InteractionValidationConfigTest.java index 0bc09b0f28d..ef5921c9187 100644 --- a/fhir-server/src/test/java/com/ibm/fhir/server/test/InteractionValidationConfigTest.java +++ b/fhir-server/src/test/java/com/ibm/fhir/server/test/InteractionValidationConfigTest.java @@ -69,7 +69,7 @@ public class InteractionValidationConfigTest { @BeforeClass void setup() throws FHIRException { FHIRConfiguration.setConfigHome("src/test/resources"); - FHIRRegistry.getInstance().register(new MockRegistryResourceProvider()); + FHIRRegistry.getInstance().addProvider(new MockRegistryResourceProvider()); persistence = new MockPersistenceImpl(); helper = new FHIRRestHelper(persistence); } diff --git a/fhir-server/src/test/java/com/ibm/fhir/server/test/ProfileValidationConfigTest.java b/fhir-server/src/test/java/com/ibm/fhir/server/test/ProfileValidationConfigTest.java index 46de5ecacf4..662ef51c941 100644 --- a/fhir-server/src/test/java/com/ibm/fhir/server/test/ProfileValidationConfigTest.java +++ b/fhir-server/src/test/java/com/ibm/fhir/server/test/ProfileValidationConfigTest.java @@ -67,7 +67,7 @@ public class ProfileValidationConfigTest { void setup() throws FHIRException { FHIRConfiguration.setConfigHome("src/test/resources"); FHIRRequestContext.get().setTenantId("profileValidationConfigTest"); - FHIRRegistry.getInstance().register(new MockRegistryResourceProvider()); + FHIRRegistry.getInstance().addProvider(new MockRegistryResourceProvider()); persistence = new MockPersistenceImpl(); helper = new FHIRRestHelper(persistence); } 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 22c445b970f..1fbcc4f0694 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 @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2019, 2020 + * (C) Copyright IBM Corp. 2019, 2021 * * SPDX-License-Identifier: Apache-2.0 */ @@ -45,6 +45,7 @@ public final class CodeSystemSupport { private static final Logger log = Logger.getLogger(CodeSystemSupport.class.getName()); private static final Map CASE_SENSITIVITY_CACHE = createLRUCache(2048); + private static final Pattern IN_COMBINING_DIACRITICAL_MARKS_PATTERN = Pattern.compile("\\p{InCombiningDiacriticalMarks}+"); private CodeSystemSupport() { } @@ -355,7 +356,7 @@ public static Set getConcepts(Concept concept) { */ public static java.lang.String normalize(java.lang.String value) { if (value != null) { - return Normalizer.normalize(value, Form.NFD).replaceAll("\\p{InCombiningDiacriticalMarks}+", "").toLowerCase(); + return IN_COMBINING_DIACRITICAL_MARKS_PATTERN.matcher(Normalizer.normalize(value, Form.NFD)).replaceAll("").toLowerCase(); } return null; } @@ -734,4 +735,4 @@ public boolean accept(Concept concept) { return false; } } -} \ No newline at end of file +} diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java b/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java index 841d85f65b8..e8389202137 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java @@ -14,6 +14,7 @@ import java.time.ZoneOffset; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -23,7 +24,6 @@ import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; import com.ibm.fhir.model.resource.CodeSystem; import com.ibm.fhir.model.resource.CodeSystem.Concept; @@ -114,11 +114,16 @@ public static boolean isExpandable(ValueSet valueSet) { } private static Set getCodeSystemReferences(List includesAndExcludes) { - return includesAndExcludes.stream() - .filter(includeOrExclude -> includeOrExclude.getConcept().isEmpty()) - .map(ValueSetSupport::getCodeSystemReference) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); + Set codeSystemReferences = new LinkedHashSet<>(); + for (Include includeOrExclude : includesAndExcludes) { + if (includeOrExclude.getConcept().isEmpty()) { + java.lang.String codeSystemReference = getCodeSystemReference(includeOrExclude); + if (codeSystemReference != null) { + codeSystemReferences.add(codeSystemReference); + } + } + } + return codeSystemReferences; } private static java.lang.String getCodeSystemReference(Include includeOrExclude) { @@ -133,28 +138,31 @@ private static java.lang.String getCodeSystemReference(Include includeOrExclude) } private static Set getValueSetReferences(List includesAndExcludes) { - return includesAndExcludes.stream() - .map(includeOrExclude -> includeOrExclude.getValueSet()) - .flatMap(List::stream) - .map(canonical -> canonical.getValue()) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); + Set valueSetReferences = new LinkedHashSet<>(); + for (Include includeOrExclude : includesAndExcludes) { + for (Canonical canonical : includeOrExclude.getValueSet()) { + if (canonical.getValue() != null) { + valueSetReferences.add(canonical.getValue()); + } + } + } + return valueSetReferences; } /** - * Get a set containing {@link ValueSet.Expansion.Contains} instances where all structural + * Get a list containing {@link ValueSet.Expansion.Contains} instances where all structural * hierarchies have been flattened. * * @param expansion * the expansion containing the list of Contains instances to flatten * @return - * flattened set of Contains instances for the given expansion + * flattened list of Contains instances for the given expansion */ - public static Set getContains(Expansion expansion) { + public static List getContains(Expansion expansion) { if (expansion == null) { - return Collections.emptySet(); + return Collections.emptyList(); } - Set result = (expansion.getTotal() != null) ? new LinkedHashSet<>(expansion.getTotal().getValue()) : new LinkedHashSet<>(); + List result = (expansion.getTotal() != null) ? new ArrayList<>(expansion.getTotal().getValue()) : new ArrayList<>(); for (Expansion.Contains contains : expansion.getContains()) { result.addAll(getContains(contains)); } @@ -267,11 +275,11 @@ private static Set expand(Include includeOrExclude) { return !systemContains.isEmpty() ? systemContains : valueSetContains; } - private static Set getContains(Expansion.Contains contains) { + private static List getContains(Expansion.Contains contains) { if (contains == null) { - return Collections.emptySet(); + return Collections.emptyList(); } - Set result = new LinkedHashSet<>(); + List result = new ArrayList<>(); result.add(contains); for (Expansion.Contains c : contains.getContains()) { result.addAll(getContains(c)); @@ -296,16 +304,20 @@ private static Expansion.Contains unwrap(Contains contains) { return contains.getContains(); } - private static Set wrap(Set unwrapped) { - return unwrapped.stream() - .map(ValueSetSupport::wrap) - .collect(Collectors.toCollection(LinkedHashSet::new)); + private static List wrap(Collection unwrapped) { + List wrapped = new ArrayList<>(unwrapped.size()); + for (Expansion.Contains contains : unwrapped) { + wrapped.add(wrap(contains)); + } + return wrapped; } - private static Set unwrap(Set wrapped) { - return wrapped.stream() - .map(ValueSetSupport::unwrap) - .collect(Collectors.toCollection(LinkedHashSet::new)); + private static List unwrap(Collection wrapped) { + List unwrapped = new ArrayList<>(wrapped.size()); + for (Contains contains : wrapped) { + unwrapped.add(unwrap(contains)); + } + return unwrapped; } private static class Contains { @@ -463,4 +475,4 @@ private static Map> computeCodeSetMap(Va } return Collections.emptyMap(); } -} \ No newline at end of file +} diff --git a/fhir-term/src/test/java/com/ibm/fhir/term/service/test/ExpansionPerformanceTest.java b/fhir-term/src/test/java/com/ibm/fhir/term/service/test/ExpansionPerformanceTest.java new file mode 100644 index 00000000000..5e27f376937 --- /dev/null +++ b/fhir-term/src/test/java/com/ibm/fhir/term/service/test/ExpansionPerformanceTest.java @@ -0,0 +1,30 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.term.service.test; + +import static com.ibm.fhir.term.util.ValueSetSupport.getValueSet; + +import com.ibm.fhir.model.resource.ValueSet; +import com.ibm.fhir.term.service.FHIRTermService; + +public class ExpansionPerformanceTest { + public static final int ITERATIONS = 1000000; + + public static void main(String[] args) { + long start = System.currentTimeMillis(); + + ValueSet valueSet = getValueSet("http://ibm.com/fhir/ValueSet/vs4|1.0.0"); + + for (int i = 0; i < ITERATIONS; i++) { + FHIRTermService.getInstance().expand(valueSet); + } + + long end = System.currentTimeMillis(); + + System.out.println("Processing time for " + ITERATIONS + " iterations: " + (end - start) + " milliseconds"); + } +} From 5fa8f939c9d735b62db82465a2c92c4ee0b0c62a Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Fri, 19 Mar 2021 10:43:27 -0400 Subject: [PATCH 38/50] Issue #1980 - sort members Signed-off-by: John T.E. Timm --- .../com/ibm/fhir/registry/FHIRRegistry.java | 162 ++++----- .../ibm/fhir/term/util/ValueSetSupport.java | 314 +++++++++--------- 2 files changed, 237 insertions(+), 239 deletions(-) diff --git a/fhir-registry/src/main/java/com/ibm/fhir/registry/FHIRRegistry.java b/fhir-registry/src/main/java/com/ibm/fhir/registry/FHIRRegistry.java index 1c6d139ae7f..d37b38e44b6 100644 --- a/fhir-registry/src/main/java/com/ibm/fhir/registry/FHIRRegistry.java +++ b/fhir-registry/src/main/java/com/ibm/fhir/registry/FHIRRegistry.java @@ -46,19 +46,6 @@ private FHIRRegistry() { providers = new CopyOnWriteArrayList<>(loadProviders()); } - /** - * Get the singleton instance of this class - * - *

This first time that this method is called, all registry resource providers made available through the - * service loader are added to the registry - * - * @return - * the singleton instance of this class - */ - public static FHIRRegistry getInstance() { - return INSTANCE; - } - /** * Add a registry resource provider to the registry * @@ -74,60 +61,69 @@ public void addProvider(FHIRRegistryResourceProvider provider) { } /** - * Indicates whether a resource for the given canonical url and resource type exists in the registry + * Get the latest version of a resource for the given url and resource type * * @param url - * the canonical url + * the url * @param resourceType * the resource type * @return - * true if a resource for the given canonical url and resource type exists in the registry, false otherwise + * the latest version of a resource for the given url and resource type if exists, null otherwise */ - public boolean hasResource(String url, Class resourceType) { + public String getLatestVersion(String url, Class resourceType) { if (url == null || resourceType == null || !isDefinitionalResourceType(resourceType)) { - return false; - } - - String id = null; - int index = url.indexOf("#"); - if (index != -1) { - id = url.substring(index + 1); - url = url.substring(0, index); + return null; } - String version = null; - index = url.indexOf("|"); + int index = url.indexOf("|"); if (index != -1) { - version = url.substring(index + 1); url = url.substring(0, index); } - FHIRRegistryResource registryResource = findRegistryResource(resourceType, url, version); - return (id != null) ? (getResource(registryResource, url, id) != null) : (registryResource != null); + FHIRRegistryResource resource = findRegistryResource(resourceType, url, null); + return (resource != null) ? resource.getVersion().toString() : null; } /** - * Get the latest version of a resource for the given url and resource type + * Get a map containing sets of type specific canonical URLs for all profile resources across all providers. * - * @param url - * the url - * @param resourceType - * the resource type * @return - * the latest version of a resource for the given url and resource type if exists, null otherwise + * the map of sets */ - public String getLatestVersion(String url, Class resourceType) { - if (url == null || resourceType == null || !isDefinitionalResourceType(resourceType)) { - return null; + public Map> getProfiles() { + Map> map = new HashMap<>(); + for (FHIRRegistryResourceProvider provider : providers) { + for (FHIRRegistryResource r : provider.getProfileResources()) { + map.computeIfAbsent(r.getType(), k -> new LinkedHashSet<>()) + .add(Canonical.of(r.getUrl(), r.getVersion().toString())); + } } + return map; + } - int index = url.indexOf("|"); - if (index != -1) { - url = url.substring(0, index); + /** + * Get the profiles that constrain the given resource type as a collection of {@link Canonical} URLs + * + * @param type + * the constrained resource type + * @return + * the profiles that constrain the given type as a collection of {@link Canonical} URLs + */ + public Collection getProfiles(String type) { + Objects.requireNonNull(type); + if (!ModelSupport.isResourceType(type)) { + throw new IllegalArgumentException("The type argument must be a valid FHIR resource type name"); } - - FHIRRegistryResource resource = findRegistryResource(resourceType, url, null); - return (resource != null) ? resource.getVersion().toString() : null; + List registryResources = new ArrayList<>(); + for (FHIRRegistryResourceProvider provider : providers) { + registryResources.addAll(provider.getProfileResources(type)); + } + Collections.sort(registryResources); + List profiles = new ArrayList<>(); + for (FHIRRegistryResource registryResource : registryResources) { + profiles.add(Canonical.of(registryResource.getUrl(), registryResource.getVersion().toString())); + } + return Collections.unmodifiableList(profiles); } /** @@ -188,31 +184,6 @@ public Collection getResources(Class resourceType) { return Collections.unmodifiableList(resources); } - /** - * Get the profiles that constrain the given resource type as a collection of {@link Canonical} URLs - * - * @param type - * the constrained resource type - * @return - * the profiles that constrain the given type as a collection of {@link Canonical} URLs - */ - public Collection getProfiles(String type) { - Objects.requireNonNull(type); - if (!ModelSupport.isResourceType(type)) { - throw new IllegalArgumentException("The type argument must be a valid FHIR resource type name"); - } - List registryResources = new ArrayList<>(); - for (FHIRRegistryResourceProvider provider : providers) { - registryResources.addAll(provider.getProfileResources(type)); - } - Collections.sort(registryResources); - List profiles = new ArrayList<>(); - for (FHIRRegistryResource registryResource : registryResources) { - profiles.add(Canonical.of(registryResource.getUrl(), registryResource.getVersion().toString())); - } - return Collections.unmodifiableList(profiles); - } - /** * Get the search parameters with the given search parameter type (e.g. string, token, etc.) * @@ -236,6 +207,39 @@ public Collection getSearchParameters(String type) { } + /** + * Indicates whether a resource for the given canonical url and resource type exists in the registry + * + * @param url + * the canonical url + * @param resourceType + * the resource type + * @return + * true if a resource for the given canonical url and resource type exists in the registry, false otherwise + */ + public boolean hasResource(String url, Class resourceType) { + if (url == null || resourceType == null || !isDefinitionalResourceType(resourceType)) { + return false; + } + + String id = null; + int index = url.indexOf("#"); + if (index != -1) { + id = url.substring(index + 1); + url = url.substring(0, index); + } + + String version = null; + index = url.indexOf("|"); + if (index != -1) { + version = url.substring(index + 1); + url = url.substring(0, index); + } + + FHIRRegistryResource registryResource = findRegistryResource(resourceType, url, version); + return (id != null) ? (getResource(registryResource, url, id) != null) : (registryResource != null); + } + private FHIRRegistryResource findRegistryResource(Class resourceType, String url, String version) { if (version == null) { // find the latest version of the registry resource with the specified resourceType and url (across all providers) @@ -290,19 +294,15 @@ private List loadProviders() { } /** - * Get a map containing sets of type specific canonical URLs for all profile resources across all providers. + * Get the singleton instance of this class + * + *

This first time that this method is called, all registry resource providers made available through the + * service loader are added to the registry * * @return - * the map of sets + * the singleton instance of this class */ - public Map> getProfiles() { - Map> map = new HashMap<>(); - for (FHIRRegistryResourceProvider provider : providers) { - for (FHIRRegistryResource r : provider.getProfileResources()) { - map.computeIfAbsent(r.getType(), k -> new LinkedHashSet<>()) - .add(Canonical.of(r.getUrl(), r.getVersion().toString())); - } - } - return map; + public static FHIRRegistry getInstance() { + return INSTANCE; } } diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java b/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java index e8389202137..814081e48ac 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/util/ValueSetSupport.java @@ -76,8 +76,36 @@ public static ValueSet expand(ValueSet valueSet) { return valueSet; } - public static boolean isExpanded(ValueSet valueSet) { - return valueSet != null && valueSet.getExpansion() != null; + /** + * Get a list containing {@link ValueSet.Expansion.Contains} instances where all structural + * hierarchies have been flattened. + * + * @param expansion + * the expansion containing the list of Contains instances to flatten + * @return + * flattened list of Contains instances for the given expansion + */ + public static List getContains(Expansion expansion) { + if (expansion == null) { + return Collections.emptyList(); + } + List result = (expansion.getTotal() != null) ? new ArrayList<>(expansion.getTotal().getValue()) : new ArrayList<>(); + for (Expansion.Contains contains : expansion.getContains()) { + result.addAll(getContains(contains)); + } + return result; + } + + /** + * Get the value set associated with the given url from the FHIR registry. + * + * @param url + * the url of the value set + * @return + * the value set associated with the given input parameter, or null if no such value set exists + */ + public static ValueSet getValueSet(java.lang.String url) { + return FHIRRegistry.getInstance().getResource(url, ValueSet.class); } public static boolean isExpandable(ValueSet valueSet) { @@ -113,72 +141,16 @@ public static boolean isExpandable(ValueSet valueSet) { return true; } - private static Set getCodeSystemReferences(List includesAndExcludes) { - Set codeSystemReferences = new LinkedHashSet<>(); - for (Include includeOrExclude : includesAndExcludes) { - if (includeOrExclude.getConcept().isEmpty()) { - java.lang.String codeSystemReference = getCodeSystemReference(includeOrExclude); - if (codeSystemReference != null) { - codeSystemReferences.add(codeSystemReference); - } - } - } - return codeSystemReferences; - } - - private static java.lang.String getCodeSystemReference(Include includeOrExclude) { - if (includeOrExclude.getSystem() != null && includeOrExclude.getSystem().getValue() != null) { - StringBuilder sb = new StringBuilder(includeOrExclude.getSystem().getValue()); - if (includeOrExclude.getVersion() != null && includeOrExclude.getVersion().getValue() != null) { - sb.append("|").append(includeOrExclude.getVersion().getValue()); - } - return sb.toString(); - } - return null; - } - - private static Set getValueSetReferences(List includesAndExcludes) { - Set valueSetReferences = new LinkedHashSet<>(); - for (Include includeOrExclude : includesAndExcludes) { - for (Canonical canonical : includeOrExclude.getValueSet()) { - if (canonical.getValue() != null) { - valueSetReferences.add(canonical.getValue()); - } - } - } - return valueSetReferences; + public static boolean isExpanded(ValueSet valueSet) { + return valueSet != null && valueSet.getExpansion() != null; } - /** - * Get a list containing {@link ValueSet.Expansion.Contains} instances where all structural - * hierarchies have been flattened. - * - * @param expansion - * the expansion containing the list of Contains instances to flatten - * @return - * flattened list of Contains instances for the given expansion - */ - public static List getContains(Expansion expansion) { - if (expansion == null) { - return Collections.emptyList(); - } - List result = (expansion.getTotal() != null) ? new ArrayList<>(expansion.getTotal().getValue()) : new ArrayList<>(); - for (Expansion.Contains contains : expansion.getContains()) { - result.addAll(getContains(contains)); - } - return result; + public static boolean validateCode(ValueSet valueSet, Code code) { + return validateCode(getCodeSetMap(valueSet), code); } - /** - * Get the value set associated with the given url from the FHIR registry. - * - * @param url - * the url of the value set - * @return - * the value set associated with the given input parameter, or null if no such value set exists - */ - public static ValueSet getValueSet(java.lang.String url) { - return FHIRRegistry.getInstance().getResource(url, ValueSet.class); + public static boolean validateCode(ValueSet valueSet, Coding coding) { + return validateCode(getCodeSetMap(valueSet), coding); } private static Contains buildContains(Uri system, String version, Code code, String display) { @@ -198,7 +170,34 @@ private static Contains buildContains(Uri system, String version, Concept concep return null; } - + private static Map> computeCodeSetMap(ValueSet valueSet) { + try { + ValueSet expanded = expand(valueSet); + if (expanded == null || expanded.getExpansion() == null) { + return Collections.emptyMap(); + } + Map> codeSetMap = new LinkedHashMap<>(); + Expansion expansion = expanded.getExpansion(); + for (Expansion.Contains contains : getContains(expansion)) { + java.lang.String system = (contains.getSystem() != null) ? contains.getSystem().getValue() : null; + java.lang.String version = (contains.getVersion() != null && contains.getVersion().getValue() != null) ? contains.getVersion().getValue() : VERSION_UNKNOWN; + java.lang.String code = (contains.getCode() != null) ? contains.getCode().getValue() : null; + if (system != null && code != null) { + java.lang.String url = !VERSION_UNKNOWN.equals(version) ? system + "|" + version : system; + if (!isCaseSensitive(url)) { + code = normalize(code); + } + codeSetMap.computeIfAbsent(system + "|" + version, k -> new LinkedHashSet<>()).add(code); + } + } + return codeSetMap; + } catch (Exception e) { + java.lang.String url = (valueSet.getUrl() != null) ? valueSet.getUrl().getValue() : ""; + java.lang.String version = (valueSet.getVersion() != null) ? valueSet.getVersion().getValue() : ""; + log.log(Level.WARNING, java.lang.String.format("Unable to expand value set with url: %s and version: %s", url, version), e); + } + return Collections.emptyMap(); + } private static Set expand(Compose compose) { if (compose == null) { @@ -229,7 +228,7 @@ private static Set expand(Include includeOrExclude) { return Collections.emptySet(); } - Set systemContains = new LinkedHashSet<>(); + Set codeSystemContains = new LinkedHashSet<>(); if (includeOrExclude.getSystem() != null) { Uri system = includeOrExclude.getSystem(); String version = (includeOrExclude.getVersion() != null) ? @@ -238,7 +237,7 @@ private static Set expand(Include includeOrExclude) { for (Include.Concept concept : includeOrExclude.getConcept()) { Code code = (concept.getCode() != null) ? concept.getCode() : null; if (code != null) { - systemContains.add(buildContains(system, version, code, concept.getDisplay())); + codeSystemContains.add(buildContains(system, version, code, concept.getDisplay())); } } } else { @@ -251,7 +250,7 @@ private static Set expand(Include includeOrExclude) { for (Concept concept : FHIRTermService.getInstance().getConcepts(codeSystem, includeOrExclude.getFilter())) { Contains contains = buildContains(system, version, concept); if (contains != null) { - systemContains.add(contains); + codeSystemContains.add(contains); } } } @@ -266,13 +265,45 @@ private static Set expand(Include includeOrExclude) { } } - if (!systemContains.isEmpty() && !valueSetContains.isEmpty()) { - Set intersection = new LinkedHashSet<>(systemContains); + if (!codeSystemContains.isEmpty() && !valueSetContains.isEmpty()) { + Set intersection = new LinkedHashSet<>(codeSystemContains); intersection.retainAll(valueSetContains); return intersection; } - return !systemContains.isEmpty() ? systemContains : valueSetContains; + return !codeSystemContains.isEmpty() ? codeSystemContains : valueSetContains; + } + + private static Map> getCodeSetMap(ValueSet valueSet) { + if (valueSet.getUrl() == null || valueSet.getVersion() == null) { + return computeCodeSetMap(valueSet); + } + java.lang.String url = valueSet.getUrl().getValue() + "|" + valueSet.getVersion().getValue(); + return CODE_SET_MAP_CACHE.computeIfAbsent(url, k -> computeCodeSetMap(valueSet)); + } + + private static java.lang.String getCodeSystemReference(Include includeOrExclude) { + if (includeOrExclude.getSystem() != null && includeOrExclude.getSystem().getValue() != null) { + StringBuilder sb = new StringBuilder(includeOrExclude.getSystem().getValue()); + if (includeOrExclude.getVersion() != null && includeOrExclude.getVersion().getValue() != null) { + sb.append("|").append(includeOrExclude.getVersion().getValue()); + } + return sb.toString(); + } + return null; + } + + private static Set getCodeSystemReferences(List includesAndExcludes) { + Set codeSystemReferences = new LinkedHashSet<>(); + for (Include includeOrExclude : includesAndExcludes) { + if (includeOrExclude.getConcept().isEmpty()) { + java.lang.String codeSystemReference = getCodeSystemReference(includeOrExclude); + if (codeSystemReference != null) { + codeSystemReferences.add(codeSystemReference); + } + } + } + return codeSystemReferences; } private static List getContains(Expansion.Contains contains) { @@ -292,24 +323,20 @@ private static String getLatestVersion(Uri system) { return (version != null && !FHIRRegistryResource.NO_VERSION.toString().equals(version)) ? string(version) : null; } - private static boolean hasResource(java.lang.String url, Class resourceType) { - return FHIRRegistry.getInstance().hasResource(url, resourceType); - } - - private static Contains wrap(Expansion.Contains contains) { - return new Contains(contains); - } - - private static Expansion.Contains unwrap(Contains contains) { - return contains.getContains(); + private static Set getValueSetReferences(List includesAndExcludes) { + Set valueSetReferences = new LinkedHashSet<>(); + for (Include includeOrExclude : includesAndExcludes) { + for (Canonical canonical : includeOrExclude.getValueSet()) { + if (canonical.getValue() != null) { + valueSetReferences.add(canonical.getValue()); + } + } + } + return valueSetReferences; } - private static List wrap(Collection unwrapped) { - List wrapped = new ArrayList<>(unwrapped.size()); - for (Expansion.Contains contains : unwrapped) { - wrapped.add(wrap(contains)); - } - return wrapped; + private static boolean hasResource(java.lang.String url, Class resourceType) { + return FHIRRegistry.getInstance().hasResource(url, resourceType); } private static List unwrap(Collection wrapped) { @@ -320,48 +347,8 @@ private static List unwrap(Collection wrapped) { return unwrapped; } - private static class Contains { - private final Expansion.Contains contains; - private final int hashCode; - - public Contains(Expansion.Contains contains) { - this.contains = contains; - hashCode = Objects.hash(contains.getSystem(), contains.getVersion(), contains.getCode()); - } - - public Expansion.Contains getContains() { - return contains; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - Contains other = (Contains) obj; - return Objects.equals(contains.getSystem(), other.contains.getSystem()) && - Objects.equals(contains.getVersion(), other.contains.getVersion()) && - Objects.equals(contains.getCode(), other.contains.getCode()); - } - - @Override - public int hashCode() { - return hashCode; - } - } - - public static boolean validateCode(ValueSet valueSet, Code code) { - return validateCode(getCodeSetMap(valueSet), code); - } - - public static boolean validateCode(ValueSet valueSet, Coding coding) { - return validateCode(getCodeSetMap(valueSet), coding); + private static Expansion.Contains unwrap(Contains contains) { + return contains.getContains(); } private static boolean validateCode(Map> codeSetMap, Code code) { @@ -439,40 +426,51 @@ private static boolean validateCode(Map> return false; } - private static Map> getCodeSetMap(ValueSet valueSet) { - if (valueSet.getUrl() == null || valueSet.getVersion() == null) { - return computeCodeSetMap(valueSet); + private static List wrap(Collection unwrapped) { + List wrapped = new ArrayList<>(unwrapped.size()); + for (Expansion.Contains contains : unwrapped) { + wrapped.add(wrap(contains)); } - java.lang.String url = valueSet.getUrl().getValue() + "|" + valueSet.getVersion().getValue(); - return CODE_SET_MAP_CACHE.computeIfAbsent(url, k -> computeCodeSetMap(valueSet)); + return wrapped; } - private static Map> computeCodeSetMap(ValueSet valueSet) { - try { - ValueSet expanded = expand(valueSet); - if (expanded == null || expanded.getExpansion() == null) { - return Collections.emptyMap(); + private static Contains wrap(Expansion.Contains contains) { + return new Contains(contains); + } + + private static class Contains { + private final Expansion.Contains contains; + private final int hashCode; + + public Contains(Expansion.Contains contains) { + this.contains = contains; + hashCode = Objects.hash(contains.getSystem(), contains.getVersion(), contains.getCode()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; } - Map> codeSetMap = new LinkedHashMap<>(); - Expansion expansion = expanded.getExpansion(); - for (Expansion.Contains contains : getContains(expansion)) { - java.lang.String system = (contains.getSystem() != null) ? contains.getSystem().getValue() : null; - java.lang.String version = (contains.getVersion() != null && contains.getVersion().getValue() != null) ? contains.getVersion().getValue() : VERSION_UNKNOWN; - java.lang.String code = (contains.getCode() != null) ? contains.getCode().getValue() : null; - if (system != null && code != null) { - java.lang.String url = !VERSION_UNKNOWN.equals(version) ? system + "|" + version : system; - if (!isCaseSensitive(url)) { - code = normalize(code); - } - codeSetMap.computeIfAbsent(system + "|" + version, k -> new LinkedHashSet<>()).add(code); - } + if (obj == null) { + return false; } - return codeSetMap; - } catch (Exception e) { - java.lang.String url = (valueSet.getUrl() != null) ? valueSet.getUrl().getValue() : ""; - java.lang.String version = (valueSet.getVersion() != null) ? valueSet.getVersion().getValue() : ""; - log.log(Level.WARNING, java.lang.String.format("Unable to expand value set with url: %s and version: %s", url, version), e); + if (getClass() != obj.getClass()) { + return false; + } + Contains other = (Contains) obj; + return Objects.equals(contains.getSystem(), other.contains.getSystem()) && + Objects.equals(contains.getVersion(), other.contains.getVersion()) && + Objects.equals(contains.getCode(), other.contains.getCode()); + } + + public Expansion.Contains getContains() { + return contains; + } + + @Override + public int hashCode() { + return hashCode; } - return Collections.emptyMap(); } } From aeb927b5358451294cb2ab4cbf0dd14afc50b4fe Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Sat, 20 Mar 2021 12:43:24 -0400 Subject: [PATCH 39/50] Issue #1980 - updated copyright headers Signed-off-by: John T.E. Timm --- .../src/main/java/com/ibm/fhir/term/spi/LookupOutcome.java | 2 +- .../main/java/com/ibm/fhir/operation/term/ClosureOperation.java | 2 +- .../main/java/com/ibm/fhir/operation/term/ExpandOperation.java | 2 +- .../main/java/com/ibm/fhir/operation/term/LookupOperation.java | 2 +- .../java/com/ibm/fhir/operation/term/TranslateOperation.java | 2 +- .../ibm/fhir/operation/term/ValueSetValidateCodeOperation.java | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fhir-term/src/main/java/com/ibm/fhir/term/spi/LookupOutcome.java b/fhir-term/src/main/java/com/ibm/fhir/term/spi/LookupOutcome.java index 2ebd3591a56..bafb8244ac1 100644 --- a/fhir-term/src/main/java/com/ibm/fhir/term/spi/LookupOutcome.java +++ b/fhir-term/src/main/java/com/ibm/fhir/term/spi/LookupOutcome.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ClosureOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ClosureOperation.java index 015771c9c20..ab34b25d0e0 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ClosureOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ClosureOperation.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ExpandOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ExpandOperation.java index d8cb600a800..2a3be9f80e1 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ExpandOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ExpandOperation.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/LookupOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/LookupOperation.java index d5c5be76771..c682b010463 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/LookupOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/LookupOperation.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/TranslateOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/TranslateOperation.java index 52f6d3bc74a..399c257ec06 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/TranslateOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/TranslateOperation.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */ diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ValueSetValidateCodeOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ValueSetValidateCodeOperation.java index e5c901f7b00..502e322e957 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ValueSetValidateCodeOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/ValueSetValidateCodeOperation.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2020 + * (C) Copyright IBM Corp. 2020, 2021 * * SPDX-License-Identifier: Apache-2.0 */ From dc9b125b99baae8225fcb8a4e237979b2754f6a1 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Sat, 20 Mar 2021 13:09:24 -0400 Subject: [PATCH 40/50] Issue #1980 - formatting Signed-off-by: John T.E. Timm --- .../fhir/operation/term/LookupOperation.java | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/LookupOperation.java b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/LookupOperation.java index c682b010463..b016de1e1c0 100644 --- a/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/LookupOperation.java +++ b/operation/fhir-operation-term/src/main/java/com/ibm/fhir/operation/term/LookupOperation.java @@ -51,20 +51,20 @@ protected Parameters doInvoke( } catch(Exception e) { throw new FHIROperationException("An error occurred during the CodeSystem lookup operation", e); } - if (outcome == null) { - IssueType issueType = IssueType.NOT_FOUND.toBuilder() - .extension(Extension.builder() + throw new FHIROperationException("Coding not found") + .withIssue(OperationOutcome.Issue.builder() + .severity(IssueSeverity.ERROR) + .code(IssueType.NOT_FOUND.toBuilder() + .extension(Extension.builder() .url(FHIRRestHelper.EXTENSION_URL + "/not-found-detail") - .value(Code.of("coding")).build()).build(); - - throw new FHIROperationException("Coding not found").withIssue( - OperationOutcome.Issue.builder().severity(IssueSeverity.ERROR).code(issueType) - .details(CodeableConcept.builder().text(string(String.format("Code '%s' in System '%s' not found." - , coding.getCode().getValue() - , coding.getSystem().getValue() - ))).build()) - .build()); + .value(Code.of("coding")) + .build()) + .build()) + .details(CodeableConcept.builder() + .text(string(String.format("Code '%s' in System '%s' not found.", coding.getCode().getValue(), coding.getSystem().getValue()))) + .build()) + .build()); } return outcome.toParameters(); } From b2a25e12d08976fdfc4d9d4796f69676a734b07a Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Mon, 22 Mar 2021 09:16:06 -0400 Subject: [PATCH 41/50] Issue #1980 - updated GraphTermServiceProvider.closure Signed-off-by: John T.E. Timm --- .../graph/provider/GraphTermServiceProvider.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 bbd4b58283b..c089fcd5c10 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 @@ -90,18 +90,21 @@ public GraphTermServiceProvider(FHIRTermGraph graph, int timeLimit) { this.timeLimit = timeLimit; } + @SuppressWarnings("unchecked") @Override public Set closure(CodeSystem codeSystem, Code code) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); Set concepts = new LinkedHashSet<>(); - concepts.add(getConcept(codeSystem, code, false, false)); - GraphTraversal g = whereCodeSystem(hasCode(vertices(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem) - .repeat(__.in(FHIRTermGraph.IS_A) - .simplePath() - .dedup()) - .emit() + boolean caseSensitive = isCaseSensitive(codeSystem); + + GraphTraversal g = whereCodeSystem(hasCode(vertices(), code.getValue(), caseSensitive), codeSystem) + .union(__.identity(), whereCodeSystem(hasCode(vertices(), code.getValue(), caseSensitive), codeSystem) + .repeat(__.in(FHIRTermGraph.IS_A) + .simplePath() + .dedup()) + .emit()) .timeLimit(timeLimit); TimeLimitStep timeLimitStep = getTimeLimitStep(g); From 12873e6f2ed2bd7a27146403f9ba714c039fabd7 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Mon, 22 Mar 2021 14:46:43 -0400 Subject: [PATCH 42/50] Issue #1980 - fixed issue when using multiple filters Signed-off-by: John T.E. Timm --- .../provider/GraphTermServiceProvider.java | 59 ++++++++---- .../term/graph/test/FHIRTermGraphTest.java | 2 + .../test/GraphTermServiceProviderTest.java | 90 +++++++++++++++++++ 3 files changed, 135 insertions(+), 16 deletions(-) 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 c089fcd5c10..5d9b1a84b49 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 @@ -150,6 +150,7 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { boolean caseSensitive = isCaseSensitive(codeSystem); + boolean first = true; for (Filter filter : filters) { Code property = filter.getProperty(); com.ibm.fhir.model.type.String value = filter.getValue(); @@ -175,16 +176,24 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem).out(FHIRTermGraph.IS_A); } else { Element element = toElement(value, type); - g = whereCodeSystem(g.has(getPropertyKey(type), element.is(DateTime.class) ? toLong(element.as(DateTime.class)) : toObject(element)).in("property_"), codeSystem); + if (first) { + g = whereCodeSystem(g.has(getPropertyKey(type), element.is(DateTime.class) ? toLong(element.as(DateTime.class)) : toObject(element)).in("property_"), codeSystem); + } else { + g = whereCodeSystem(g.where(__.out("property_").has(getPropertyKey(type), element.is(DateTime.class) ? toLong(element.as(DateTime.class)) : toObject(element))), codeSystem); + } } } break; case EXISTS: if (hasCodeSystemProperty(codeSystem, property) && convertsToBoolean(value)) { if (Boolean.valueOf(value.getValue())) { - g = whereCodeSystem(g.has("code", property.getValue()).in("property_"), codeSystem); + if (first) { + g = whereCodeSystem(g.has("code", property.getValue()).in("property_"), codeSystem); + } else { + g = whereCodeSystem(g.where(__.out("property_").has("code", property.getValue())), codeSystem); + } } else { - g = whereCodeSystem(g.not(__.out("property_").has("code", property.getValue())), codeSystem); + g = whereCodeSystem(g.not(__.out("property_").has("code", property.getValue())).hasLabel("Concept"), codeSystem); } } break; @@ -211,10 +220,17 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { .collect(Collectors.toSet()))), codeSystem); } } else { - g = whereCodeSystem(g.has(getPropertyKey(type), P.within(Arrays.stream(value.getValue().split(",")) - .map(v -> toElement(v, type)) - .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) - .collect(Collectors.toSet()))).in("property_"), codeSystem); + if (first) { + g = whereCodeSystem(g.has(getPropertyKey(type), P.within(Arrays.stream(value.getValue().split(",")) + .map(v -> toElement(v, type)) + .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) + .collect(Collectors.toSet()))).in("property_"), codeSystem); + } else { + g = whereCodeSystem(g.where(__.out("property_").has(getPropertyKey(type), P.within(Arrays.stream(value.getValue().split(",")) + .map(v -> toElement(v, type)) + .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) + .collect(Collectors.toSet())))), codeSystem); + } } } break; @@ -234,7 +250,8 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { g = whereCodeSystem(g.not(__.repeat(__.out(FHIRTermGraph.IS_A).simplePath()) .until(hasCode(value.getValue(), caseSensitive))) - .not(hasCode(value.getValue(), caseSensitive)), codeSystem); + .not(hasCode(value.getValue(), caseSensitive)), codeSystem) + .hasLabel("Concept"); } break; case NOT_IN: @@ -249,24 +266,34 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { .collect(Collectors.toSet()))), codeSystem); } } else { - g = whereCodeSystem(g.has(getPropertyKey(type), P.without(Arrays.stream(value.getValue().split(",")) - .map(v -> toElement(v, type)) - .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) - .collect(Collectors.toSet()))).in("property_"), codeSystem); + if (first) { + g = whereCodeSystem(g.has(getPropertyKey(type), P.without(Arrays.stream(value.getValue().split(",")) + .map(v -> toElement(v, type)) + .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) + .collect(Collectors.toSet()))).in("property_"), codeSystem); + } else { + g = whereCodeSystem(g.where(__.out("property_").has(getPropertyKey(type), P.without(Arrays.stream(value.getValue().split(",")) + .map(v -> toElement(v, type)) + .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) + .collect(Collectors.toSet())))), codeSystem); + } } } break; case REGEX: if (hasCodeSystemProperty(codeSystem, property) && (PropertyType.CODE.equals(type) || PropertyType.STRING.equals(type))) { - g = whereCodeSystem(g.has(getPropertyKey(type), Text.textRegex(value.getValue())).in("property_"), codeSystem); + if (first) { + g = whereCodeSystem(g.has(getPropertyKey(type), Text.textRegex(value.getValue())).in("property_"), codeSystem); + } else { + g = whereCodeSystem(g.where(__.out("property_").has(getPropertyKey(type), Text.textRegex(value.getValue()))), codeSystem); + } } break; - default: - break; } + first = false; } - g = g.hasLabel("Concept").timeLimit(timeLimit); + g = g.timeLimit(timeLimit); TimeLimitStep timeLimitStep = getTimeLimitStep(g); g.elementMap().toStream().forEach(elementMap -> concepts.add(createConcept(elementMap))); diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java index 17e8c08fdc4..e60182c4c97 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/FHIRTermGraphTest.java @@ -131,6 +131,8 @@ public static void main(String[] args) throws Exception { System.out.println(""); + g.V().not(__.out("property_").has("code", "someCode")).hasLabel("Concept").elementMap().toStream().forEach(System.out::println); + graph.close(); } } diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java index 6116ac04068..61fab171c6d 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java @@ -178,6 +178,27 @@ public void testGetConceptsWithChildEqualsFilter() { Assert.assertEquals(actual, expected); } + @Test + public void testGetConceptsWithMultipleChildEqualsFilter() { + Set concepts = provider.getConcepts(codeSystem, Arrays.asList( + Filter.builder() + .property(Code.of("child")) + .op(FilterOperator.EQUALS) + .value(string("g")) + .build(), + Filter.builder() + .property(Code.of("child")) + .op(FilterOperator.EQUALS) + .value(string("h")) + .build())); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + Assert.assertEquals(actual, Collections.emptyList()); + } + @Test public void testGetConceptsWithBooleanPropertyEqualsFilter() { Filter filter = Filter.builder() @@ -501,6 +522,75 @@ public void testGetConceptsWithIsAFilter() { Assert.assertEquals(actual, expected); } + @Test + public void testGetConceptsWithMulitpleIsAFilters() { + Set concepts = provider.getConcepts(codeSystem, Arrays.asList( + Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IS_A) + .value(string("d")) + .build(), + Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IS_A) + .value(string("r")) + .build())); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("r", "s"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithIsAFilterAndDateTimePropertyEqualsFilter() { + Set concepts = provider.getConcepts(codeSystem, Arrays.asList( + Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IS_A) + .value(string("a")) + .build(), + Filter.builder() + .property(Code.of("dateTimeProperty")) + .op(FilterOperator.EQUALS) + .value(string("2021-01-01T00:00:00.000Z")) + .build())); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("k"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithIsAFilterAndConceptInFilter() { + Set concepts = provider.getConcepts(codeSystem, Arrays.asList( + Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IS_A) + .value(string("d")) + .build(), + Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IN) + .value(string("q,s")) + .build())); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("q", "s"); + + Assert.assertEquals(actual, expected); + } + @Test public void testGetConceptsWithIsNotAFilter() { Filter filter = Filter.builder() From a6ee31f4e3eb89beac7bd2e1a1b21a402306b0a0 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Mon, 22 Mar 2021 16:27:59 -0400 Subject: [PATCH 43/50] Issue #1980 - fixed issue with is-not-a filter Signed-off-by: John T.E. Timm --- .../provider/GraphTermServiceProvider.java | 6 +-- .../test/GraphTermServiceProviderTest.java | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) 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 5d9b1a84b49..88235f0f4ac 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 @@ -248,10 +248,10 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { case IS_NOT_A: // not descendants or self if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - g = whereCodeSystem(g.not(__.repeat(__.out(FHIRTermGraph.IS_A).simplePath()) + g = whereCodeSystem(g.not(__.repeat(__.out(FHIRTermGraph.IS_A)) .until(hasCode(value.getValue(), caseSensitive))) - .not(hasCode(value.getValue(), caseSensitive)), codeSystem) - .hasLabel("Concept"); + .not(hasCode(value.getValue(), caseSensitive)) + .hasLabel("Concept"), codeSystem); } break; case NOT_IN: diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java index 61fab171c6d..44e8083d68b 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java @@ -545,6 +545,29 @@ public void testGetConceptsWithMulitpleIsAFilters() { Assert.assertEquals(actual, expected); } + @Test + public void testGetConceptsWithIsAFilterAndIsNotAFilter() { + Set concepts = provider.getConcepts(codeSystem, Arrays.asList( + Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IS_A) + .value(string("d")) + .build(), + Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IS_NOT_A) + .value(string("r")) + .build())); + + List actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toList()); + + List expected = Arrays.asList("d", "q"); + + Assert.assertEquals(actual, expected); + } + @Test public void testGetConceptsWithIsAFilterAndDateTimePropertyEqualsFilter() { Set concepts = provider.getConcepts(codeSystem, Arrays.asList( @@ -568,6 +591,29 @@ public void testGetConceptsWithIsAFilterAndDateTimePropertyEqualsFilter() { Assert.assertEquals(actual, expected); } + @Test + public void testGetConceptsWithIsAFilterAndNotExistsFilter() { + Set concepts = provider.getConcepts(codeSystem, Arrays.asList( + Filter.builder() + .property(Code.of("concept")) + .op(FilterOperator.IS_A) + .value(string("a")) + .build(), + Filter.builder() + .property(Code.of("stringProperty")) + .op(FilterOperator.EXISTS) + .value(string("false")) + .build())); + + Set actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toSet()); + + Set expected = new HashSet<>(Arrays.asList("g", "h", "i", "j", "k")); + + Assert.assertEquals(actual, expected); + } + @Test public void testGetConceptsWithIsAFilterAndConceptInFilter() { Set concepts = provider.getConcepts(codeSystem, Arrays.asList( From 46ab053dcab5bed3be611688dffe578eb6e7a6d1 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 23 Mar 2021 09:35:45 -0400 Subject: [PATCH 44/50] Issue #1980 - updated "parent" / "child" equals behavior Signed-off-by: John T.E. Timm --- .../provider/GraphTermServiceProvider.java | 30 +++++++++++++++++-- .../test/GraphTermServiceProviderTest.java | 29 +++++++++++++++++- 2 files changed, 55 insertions(+), 4 deletions(-) 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 88235f0f4ac..25af76813bf 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 @@ -139,7 +139,7 @@ public Set getConcepts(CodeSystem codeSystem) { return concepts; } - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public Set getConcepts(CodeSystem codeSystem, List filters) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); @@ -171,9 +171,27 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { if ((("parent".equals(property.getValue()) || "child".equals(property.getValue())) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) || (hasCodeSystemProperty(codeSystem, property) && !PropertyType.CODING.equals(type))) { if ("parent".equals(property.getValue())) { - g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem).in(FHIRTermGraph.IS_A); + if (first) { + g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem).in(FHIRTermGraph.IS_A); + } else { + // intersection with previous results + g = (GraphTraversal) whereCodeSystem(hasCode(g.as("a").V(), value.getValue(), caseSensitive), codeSystem) + .in(FHIRTermGraph.IS_A) + .as("b") + .select("a") + .where("a", P.eq("b")); + } } else if ("child".equals(property.getValue())) { - g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem).out(FHIRTermGraph.IS_A); + if (first) { + g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem).out(FHIRTermGraph.IS_A); + } else { + // intersection with previous results + g = (GraphTraversal) whereCodeSystem(hasCode(g.as("a").V(), value.getValue(), caseSensitive), codeSystem) + .out(FHIRTermGraph.IS_A) + .as("b") + .select("a") + .where("a", P.eq("b")); + } } else { Element element = toElement(value, type); if (first) { @@ -248,6 +266,12 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { case IS_NOT_A: // not descendants or self if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + /* + // alternative + g = whereCodeSystem(g.not(__.until(hasCode(value.getValue(), caseSensitive)) + .repeat((GraphTraversal) __.out(FHIRTermGraph.IS_A))) + .hasLabel("Concept"), codeSystem); + */ g = whereCodeSystem(g.not(__.repeat(__.out(FHIRTermGraph.IS_A)) .until(hasCode(value.getValue(), caseSensitive))) .not(hasCode(value.getValue(), caseSensitive)) diff --git a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java index 44e8083d68b..b47a3dd3f31 100644 --- a/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java +++ b/fhir-term-graph/src/test/java/com/ibm/fhir/term/graph/test/GraphTermServiceProviderTest.java @@ -196,7 +196,34 @@ public void testGetConceptsWithMultipleChildEqualsFilter() { .map(concept -> concept.getCode().getValue()) .collect(Collectors.toList()); - Assert.assertEquals(actual, Collections.emptyList()); + // intersection + List expected = Arrays.asList("a"); + + Assert.assertEquals(actual, expected); + } + + @Test + public void testGetConceptsWithMultipleParentEqualsFilter() { + Set concepts = provider.getConcepts(codeSystem, Arrays.asList( + Filter.builder() + .property(Code.of("parent")) + .op(FilterOperator.EQUALS) + .value(string("a")) + .build(), + Filter.builder() + .property(Code.of("parent")) + .op(FilterOperator.EQUALS) + .value(string("a")) + .build())); + + Set actual = concepts.stream() + .map(concept -> concept.getCode().getValue()) + .collect(Collectors.toSet()); + + // intersection + Set expected = new HashSet<>(Arrays.asList("g", "h", "i")); + + Assert.assertEquals(actual, expected); } @Test From 21b8b3d114b6064807073a171c4e057af74d38c4 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 23 Mar 2021 11:45:20 -0400 Subject: [PATCH 45/50] Issue #1980 - formatting / clean-up Signed-off-by: John T.E. Timm --- .../provider/GraphTermServiceProvider.java | 430 +++++++++++------- 1 file changed, 267 insertions(+), 163 deletions(-) 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 25af76813bf..0162c573374 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 @@ -25,6 +25,8 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; import org.apache.commons.configuration.Configuration; @@ -62,6 +64,8 @@ * Graph-based implementation of the {@link FHIRTermServiceProvider} interface using {@link FHIRTermGraph} */ public class GraphTermServiceProvider implements FHIRTermServiceProvider { + private static final Logger log = Logger.getLogger(GraphTermServiceProvider.class.getName()); + public static final int DEFAULT_TIME_LIMIT = 90000; // 90 seconds private static final int DEFAULT_COUNT = 1000; @@ -100,12 +104,12 @@ public Set closure(CodeSystem codeSystem, Code code) { boolean caseSensitive = isCaseSensitive(codeSystem); GraphTraversal g = whereCodeSystem(hasCode(vertices(), code.getValue(), caseSensitive), codeSystem) - .union(__.identity(), whereCodeSystem(hasCode(vertices(), code.getValue(), caseSensitive), codeSystem) - .repeat(__.in(FHIRTermGraph.IS_A) - .simplePath() - .dedup()) - .emit()) - .timeLimit(timeLimit); + .union(__.identity(), whereCodeSystem(hasCode(vertices(), code.getValue(), caseSensitive), codeSystem) + .repeat(__.in(FHIRTermGraph.IS_A) + .simplePath() + .dedup()) + .emit()) + .timeLimit(timeLimit); TimeLimitStep timeLimitStep = getTimeLimitStep(g); g.elementMap().toStream().forEach(elementMap -> concepts.add(createConcept(elementMap))); @@ -128,8 +132,8 @@ public Set getConcepts(CodeSystem codeSystem) { Set concepts = new LinkedHashSet<>(getCount(codeSystem)); GraphTraversal g = hasVersion(hasUrl(vertices(), codeSystem.getUrl()), codeSystem.getVersion()) - .out("concept") - .timeLimit(timeLimit); + .out("concept") + .timeLimit(timeLimit); TimeLimitStep timeLimitStep = getTimeLimitStep(g); g.elementMap().toStream().forEach(elementMap -> concepts.add(createConcept(elementMap))); @@ -139,7 +143,6 @@ public Set getConcepts(CodeSystem codeSystem) { return concepts; } - @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public Set getConcepts(CodeSystem codeSystem, List filters) { Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); @@ -148,170 +151,39 @@ public Set getConcepts(CodeSystem codeSystem, List filters) { GraphTraversal g = vertices(); - boolean caseSensitive = isCaseSensitive(codeSystem); - boolean first = true; for (Filter filter : filters) { - Code property = filter.getProperty(); - com.ibm.fhir.model.type.String value = filter.getValue(); - PropertyType type = getCodeSystemPropertyType(codeSystem, property); - switch (filter.getOp().getValueAsEnumConstant()) { case DESCENDENT_OF: // descendants - if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) - .repeat(__.in(FHIRTermGraph.IS_A) - .simplePath() - .dedup()) - .emit(); - } + g = applyDescendentOfFilter(codeSystem, filter, g); break; case EQUALS: - if ((("parent".equals(property.getValue()) || "child".equals(property.getValue())) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) - || (hasCodeSystemProperty(codeSystem, property) && !PropertyType.CODING.equals(type))) { - if ("parent".equals(property.getValue())) { - if (first) { - g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem).in(FHIRTermGraph.IS_A); - } else { - // intersection with previous results - g = (GraphTraversal) whereCodeSystem(hasCode(g.as("a").V(), value.getValue(), caseSensitive), codeSystem) - .in(FHIRTermGraph.IS_A) - .as("b") - .select("a") - .where("a", P.eq("b")); - } - } else if ("child".equals(property.getValue())) { - if (first) { - g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem).out(FHIRTermGraph.IS_A); - } else { - // intersection with previous results - g = (GraphTraversal) whereCodeSystem(hasCode(g.as("a").V(), value.getValue(), caseSensitive), codeSystem) - .out(FHIRTermGraph.IS_A) - .as("b") - .select("a") - .where("a", P.eq("b")); - } - } else { - Element element = toElement(value, type); - if (first) { - g = whereCodeSystem(g.has(getPropertyKey(type), element.is(DateTime.class) ? toLong(element.as(DateTime.class)) : toObject(element)).in("property_"), codeSystem); - } else { - g = whereCodeSystem(g.where(__.out("property_").has(getPropertyKey(type), element.is(DateTime.class) ? toLong(element.as(DateTime.class)) : toObject(element))), codeSystem); - } - } - } + g = applyEqualsFilter(codeSystem, filter, first, g); break; case EXISTS: - if (hasCodeSystemProperty(codeSystem, property) && convertsToBoolean(value)) { - if (Boolean.valueOf(value.getValue())) { - if (first) { - g = whereCodeSystem(g.has("code", property.getValue()).in("property_"), codeSystem); - } else { - g = whereCodeSystem(g.where(__.out("property_").has("code", property.getValue())), codeSystem); - } - } else { - g = whereCodeSystem(g.not(__.out("property_").has("code", property.getValue())).hasLabel("Concept"), codeSystem); - } - } + g = applyExistsFilter(codeSystem, filter, first, g); break; case GENERALIZES: // ancestors and self - if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) - .union(__.identity(), whereCodeSystem(hasCode(vertices(), value.getValue(), caseSensitive), codeSystem) - .repeat(__.out(FHIRTermGraph.IS_A) - .simplePath() - .dedup()) - .emit()); - } + g = applyGeneralizesFilter(codeSystem, filter, g); break; case IN: - if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { - if ("concept".equals(property.getValue())) { - if (caseSensitive) { - g = whereCodeSystem(g.has("code", P.within(Arrays.stream(value.getValue().split(",")) - .collect(Collectors.toSet()))), codeSystem); - } else { - g = whereCodeSystem(g.has("codeLowerCase", P.within(Arrays.stream(value.getValue().split(",")) - .map(CodeSystemSupport::normalize) - .collect(Collectors.toSet()))), codeSystem); - } - } else { - if (first) { - g = whereCodeSystem(g.has(getPropertyKey(type), P.within(Arrays.stream(value.getValue().split(",")) - .map(v -> toElement(v, type)) - .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) - .collect(Collectors.toSet()))).in("property_"), codeSystem); - } else { - g = whereCodeSystem(g.where(__.out("property_").has(getPropertyKey(type), P.within(Arrays.stream(value.getValue().split(",")) - .map(v -> toElement(v, type)) - .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) - .collect(Collectors.toSet())))), codeSystem); - } - } - } + g = applyInFilter(codeSystem, filter, first, g); break; case IS_A: // descendants and self - if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - g = whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) - .union(__.identity(), whereCodeSystem(hasCode(vertices(), value.getValue(), caseSensitive), codeSystem) - .repeat(__.in(FHIRTermGraph.IS_A) - .simplePath() - .dedup()) - .emit()); - } + g = applyIsAFilter(codeSystem, filter, g); break; case IS_NOT_A: // not descendants or self - if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - /* - // alternative - g = whereCodeSystem(g.not(__.until(hasCode(value.getValue(), caseSensitive)) - .repeat((GraphTraversal) __.out(FHIRTermGraph.IS_A))) - .hasLabel("Concept"), codeSystem); - */ - g = whereCodeSystem(g.not(__.repeat(__.out(FHIRTermGraph.IS_A)) - .until(hasCode(value.getValue(), caseSensitive))) - .not(hasCode(value.getValue(), caseSensitive)) - .hasLabel("Concept"), codeSystem); - } + g = applyIsNotAFilter(codeSystem, filter, g); break; case NOT_IN: - if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { - if ("concept".equals(property.getValue())) { - if (caseSensitive) { - g = whereCodeSystem(g.has("code", P.without(Arrays.stream(value.getValue().split(",")) - .collect(Collectors.toSet()))), codeSystem); - } else { - g = whereCodeSystem(g.has("codeLowerCase", P.without(Arrays.stream(value.getValue().split(",")) - .map(CodeSystemSupport::normalize) - .collect(Collectors.toSet()))), codeSystem); - } - } else { - if (first) { - g = whereCodeSystem(g.has(getPropertyKey(type), P.without(Arrays.stream(value.getValue().split(",")) - .map(v -> toElement(v, type)) - .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) - .collect(Collectors.toSet()))).in("property_"), codeSystem); - } else { - g = whereCodeSystem(g.where(__.out("property_").has(getPropertyKey(type), P.without(Arrays.stream(value.getValue().split(",")) - .map(v -> toElement(v, type)) - .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) - .collect(Collectors.toSet())))), codeSystem); - } - } - } + g = applyNotInFilter(codeSystem, filter, first, g); break; case REGEX: - if (hasCodeSystemProperty(codeSystem, property) && (PropertyType.CODE.equals(type) || PropertyType.STRING.equals(type))) { - if (first) { - g = whereCodeSystem(g.has(getPropertyKey(type), Text.textRegex(value.getValue())).in("property_"), codeSystem); - } else { - g = whereCodeSystem(g.where(__.out("property_").has(getPropertyKey(type), Text.textRegex(value.getValue()))), codeSystem); - } - } + g = applyRegexFilter(codeSystem, filter, first, g); break; } first = false; @@ -357,10 +229,10 @@ public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { } GraphTraversal g = whereCodeSystem(hasCode(vertices(), codeA.getValue(), caseSensitive), codeSystem) - .repeat(__.in(FHIRTermGraph.IS_A) - .simplePath()) - .until(hasCode(codeB.getValue(), caseSensitive)) - .timeLimit(timeLimit); + .repeat(__.in(FHIRTermGraph.IS_A) + .simplePath()) + .until(hasCode(codeB.getValue(), caseSensitive)) + .timeLimit(timeLimit); TimeLimitStep timeLimitStep = getTimeLimitStep(g); boolean subsumes = g.hasNext(); @@ -370,6 +242,233 @@ public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { return subsumes; } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private GraphTraversal applyChildEqualsFilter(CodeSystem codeSystem, Filter filter, boolean first, GraphTraversal g) { + boolean caseSensitive = isCaseSensitive(codeSystem); + com.ibm.fhir.model.type.String value = filter.getValue(); + return first ? + whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) + .out(FHIRTermGraph.IS_A) : + // intersection with previous results + (GraphTraversal) whereCodeSystem(hasCode(g.as("a").V(), value.getValue(), caseSensitive), codeSystem) + .out(FHIRTermGraph.IS_A) + .as("b") + .select("a") + .where("a", P.eq("b")); + } + + private GraphTraversal applyConceptInFilter(CodeSystem codeSystem, Filter filter, boolean first, GraphTraversal g) { + boolean caseSensitive = isCaseSensitive(codeSystem); + com.ibm.fhir.model.type.String value = filter.getValue(); + return caseSensitive ? + whereCodeSystem(g.has("code", P.within(Arrays.stream(value.getValue().split(",")) + .collect(Collectors.toSet()))), codeSystem) : + whereCodeSystem(g.has("codeLowerCase", P.within(Arrays.stream(value.getValue().split(",")) + .map(CodeSystemSupport::normalize) + .collect(Collectors.toSet()))), codeSystem); + } + + private GraphTraversal applyConceptNotInFilter(CodeSystem codeSystem, Filter filter, boolean first, GraphTraversal g) { + boolean caseSensitive = isCaseSensitive(codeSystem); + com.ibm.fhir.model.type.String value = filter.getValue(); + return caseSensitive ? + whereCodeSystem(g.has("code", P.without(Arrays.stream(value.getValue().split(",")) + .collect(Collectors.toSet()))), codeSystem) : + whereCodeSystem(g.has("codeLowerCase", P.without(Arrays.stream(value.getValue().split(",")) + .map(CodeSystemSupport::normalize) + .collect(Collectors.toSet()))), codeSystem); + } + + private GraphTraversal applyDescendentOfFilter(CodeSystem codeSystem, Filter filter, GraphTraversal g) { + boolean caseSensitive = isCaseSensitive(codeSystem); + Code property = filter.getProperty(); + com.ibm.fhir.model.type.String value = filter.getValue(); + if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + return whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) + .repeat(__.in(FHIRTermGraph.IS_A) + .simplePath() + .dedup()) + .emit(); + } + return filterNotApplied(filter, g); + } + + private GraphTraversal applyEqualsFilter(CodeSystem codeSystem, Filter filter, boolean first, GraphTraversal g) { + Code property = filter.getProperty(); + PropertyType type = getCodeSystemPropertyType(codeSystem, property); + if ((("parent".equals(property.getValue()) || "child".equals(property.getValue())) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) + || (hasCodeSystemProperty(codeSystem, property) && !PropertyType.CODING.equals(type))) { + return "parent".equals(property.getValue()) ? + applyParentEqualsFilter(codeSystem, filter, first, g) : + "child".equals(property.getValue()) ? + applyChildEqualsFilter(codeSystem, filter, first, g) : + applyPropertyEqualsFilter(codeSystem, filter, first, g); + } + return filterNotApplied(filter, g); + } + + private GraphTraversal applyExistsFilter(CodeSystem codeSystem, Filter filter, boolean first, GraphTraversal g) { + Code property = filter.getProperty(); + com.ibm.fhir.model.type.String value = filter.getValue(); + if (hasCodeSystemProperty(codeSystem, property) && convertsToBoolean(value)) { + return Boolean.valueOf(value.getValue()) ? + applyPropertyExistsFilter(codeSystem, filter, first, g) : + // not exists + applyPropertyNotExistsFilter(codeSystem, filter, g); + } + return filterNotApplied(filter, g); + } + + @SuppressWarnings("unchecked") + private GraphTraversal applyGeneralizesFilter(CodeSystem codeSystem, Filter filter, GraphTraversal g) { + boolean caseSensitive = isCaseSensitive(codeSystem); + Code property = filter.getProperty(); + com.ibm.fhir.model.type.String value = filter.getValue(); + if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + return whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) + .union(__.identity(), whereCodeSystem(hasCode(vertices(), value.getValue(), caseSensitive), codeSystem) + .repeat(__.out(FHIRTermGraph.IS_A) + .simplePath() + .dedup()) + .emit()); + } + return filterNotApplied(filter, g); + } + + private GraphTraversal applyInFilter(CodeSystem codeSystem, Filter filter, boolean first, GraphTraversal g) { + Code property = filter.getProperty(); + if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { + return "concept".equals(property.getValue()) ? + applyConceptInFilter(codeSystem, filter, first, g) : + applyPropertyInFilter(codeSystem, filter, first, g); + } + return filterNotApplied(filter, g); + } + + @SuppressWarnings("unchecked") + private GraphTraversal applyIsAFilter(CodeSystem codeSystem, Filter filter, GraphTraversal g) { + boolean caseSensitive = isCaseSensitive(codeSystem); + Code property = filter.getProperty(); + com.ibm.fhir.model.type.String value = filter.getValue(); + if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + return whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) + .union(__.identity(), whereCodeSystem(hasCode(vertices(), value.getValue(), caseSensitive), codeSystem) + .repeat(__.in(FHIRTermGraph.IS_A) + .simplePath() + .dedup()) + .emit()); + } + return filterNotApplied(filter, g); + } + + private GraphTraversal applyIsNotAFilter(CodeSystem codeSystem, Filter filter, GraphTraversal g) { + boolean caseSensitive = isCaseSensitive(codeSystem); + Code property = filter.getProperty(); + com.ibm.fhir.model.type.String value = filter.getValue(); + if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { + /* + // alternative from: https://groups.google.com/g/gremlin-users/c/hHlVRMl1JZs + return whereCodeSystem(g.not(__.until(hasCode(value.getValue(), caseSensitive)) + .repeat((GraphTraversal) __.out(FHIRTermGraph.IS_A))) + .hasLabel("Concept"), codeSystem); + */ + return whereCodeSystem(g.not(__.repeat(__.out(FHIRTermGraph.IS_A)) + .until(hasCode(value.getValue(), caseSensitive))) + .not(hasCode(value.getValue(), caseSensitive)) + .hasLabel("Concept"), codeSystem); + } + return filterNotApplied(filter, g); + } + + private GraphTraversal applyNotInFilter(CodeSystem codeSystem, Filter filter, boolean first, GraphTraversal g) { + Code property = filter.getProperty(); + if ("concept".equals(property.getValue()) || hasCodeSystemProperty(codeSystem, property)) { + return "concept".equals(property.getValue()) ? + applyConceptNotInFilter(codeSystem, filter, first, g) : + applyPropertyNotInFilter(codeSystem, filter, first, g); + } + return filterNotApplied(filter, g); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private GraphTraversal applyParentEqualsFilter(CodeSystem codeSystem, Filter filter, boolean first, GraphTraversal g) { + boolean caseSensitive = isCaseSensitive(codeSystem); + com.ibm.fhir.model.type.String value = filter.getValue(); + return first ? + whereCodeSystem(hasCode(g, value.getValue(), caseSensitive), codeSystem) + .in(FHIRTermGraph.IS_A) : + // intersection with previous results + (GraphTraversal) whereCodeSystem(hasCode(g.as("a").V(), value.getValue(), caseSensitive), codeSystem) + .in(FHIRTermGraph.IS_A) + .as("b") + .select("a") + .where("a", P.eq("b")); + } + + private GraphTraversal applyPropertyEqualsFilter(CodeSystem codeSystem, Filter filter, boolean first, GraphTraversal g) { + Code property = filter.getProperty(); + com.ibm.fhir.model.type.String value = filter.getValue(); + PropertyType type = getCodeSystemPropertyType(codeSystem, property); + Element element = toElement(value, type); + return first ? + whereCodeSystem(g.has(getPropertyKey(type), element.is(DateTime.class) ? toLong(element.as(DateTime.class)) : toObject(element)) + .in("property_"), codeSystem) : + whereCodeSystem(g.where(__.out("property_").has(getPropertyKey(type), element.is(DateTime.class) ? toLong(element.as(DateTime.class)) : toObject(element))), codeSystem); + } + + private GraphTraversal applyPropertyExistsFilter(CodeSystem codeSystem, Filter filter, boolean first, GraphTraversal g) { + Code property = filter.getProperty(); + return first ? + whereCodeSystem(g.has("code", property.getValue()).in("property_"), codeSystem) : + whereCodeSystem(g.where(__.out("property_").has("code", property.getValue())), codeSystem); + } + + private GraphTraversal applyPropertyInFilter(CodeSystem codeSystem, Filter filter, boolean first, GraphTraversal g) { + Code property = filter.getProperty(); + com.ibm.fhir.model.type.String value = filter.getValue(); + PropertyType type = getCodeSystemPropertyType(codeSystem, property); + return first ? + whereCodeSystem(g.has(getPropertyKey(type), P.within(Arrays.stream(value.getValue().split(",")) + .map(v -> toElement(v, type)) + .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) + .collect(Collectors.toSet()))).in("property_"), codeSystem) : + whereCodeSystem(g.where(__.out("property_").has(getPropertyKey(type), P.within(Arrays.stream(value.getValue().split(",")) + .map(v -> toElement(v, type)) + .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) + .collect(Collectors.toSet())))), codeSystem); + } + + private GraphTraversal applyPropertyNotExistsFilter(CodeSystem codeSystem, Filter filter, GraphTraversal g) { + return whereCodeSystem(g.not(__.out("property_").has("code", filter.getProperty().getValue())).hasLabel("Concept"), codeSystem); + } + + private GraphTraversal applyPropertyNotInFilter(CodeSystem codeSystem, Filter filter, boolean first, GraphTraversal g) { + Code property = filter.getProperty(); + com.ibm.fhir.model.type.String value = filter.getValue(); + PropertyType type = getCodeSystemPropertyType(codeSystem, property); + return first ? + whereCodeSystem(g.has(getPropertyKey(type), P.without(Arrays.stream(value.getValue().split(",")) + .map(v -> toElement(v, type)) + .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) + .collect(Collectors.toSet()))).in("property_"), codeSystem) : + whereCodeSystem(g.where(__.out("property_").has(getPropertyKey(type), P.without(Arrays.stream(value.getValue().split(",")) + .map(v -> toElement(v, type)) + .map(e -> e.is(DateTime.class) ? toLong(e.as(DateTime.class)) : toObject(e)) + .collect(Collectors.toSet())))), codeSystem); + } + + private GraphTraversal applyRegexFilter(CodeSystem codeSystem, Filter filter, boolean first, GraphTraversal g) { + Code property = filter.getProperty(); + com.ibm.fhir.model.type.String value = filter.getValue(); + PropertyType type = getCodeSystemPropertyType(codeSystem, property); + if (hasCodeSystemProperty(codeSystem, property) && (PropertyType.CODE.equals(type) || PropertyType.STRING.equals(type))) { + return first ? + whereCodeSystem(g.has(getPropertyKey(type), Text.textRegex(value.getValue())).in("property_"), codeSystem) : + whereCodeSystem(g.where(__.out("property_").has(getPropertyKey(type), Text.textRegex(value.getValue()))), codeSystem); + } + return filterNotApplied(filter, g); + } + private void checkTimeLimit(TimeLimitStep timeLimitStep) { if (timeLimitStep.getTimedOut()) { throw new FHIRTermServiceException("Graph traversal timed out", Collections.singletonList(Issue.builder() @@ -395,11 +494,11 @@ private Concept createConcept(Map elementMap) { private Concept createConcept(Map elementMap, List designations, List properties) { return Concept.builder() - .code(Code.of((String) elementMap.get("code"))) - .display(string((String) elementMap.get("display"))) - .designation(designations) - .property(properties) - .build(); + .code(Code.of((String) elementMap.get("code"))) + .display(string((String) elementMap.get("display"))) + .designation(designations) + .property(properties) + .build(); } private Designation createDesignation(Map elementMap, String designationUseSystem) { @@ -419,9 +518,14 @@ private Designation createDesignation(Map elementMap, String des private Property createProperty(Map elementMap) { return Property.builder() - .code(Code.of((String) elementMap.get("code"))) - .value(getValue(elementMap)) - .build(); + .code(Code.of((String) elementMap.get("code"))) + .value(getValue(elementMap)) + .build(); + } + + private GraphTraversal filterNotApplied(Filter filter, GraphTraversal g) { + log.log(Level.WARNING, String.format("Filter not applied - property: %s, op: %s, value: %s", filter.getProperty().getValue(), filter.getOp().getValue(), filter.getValue().getValue())); + return g; } private Concept getConcept(CodeSystem codeSystem, Code code, boolean includeDesignations, boolean includeProperties) { @@ -438,8 +542,8 @@ private Concept getConcept(CodeSystem codeSystem, Code code, boolean includeDesi private int getCount(CodeSystem codeSystem) { Optional> optional = hasVersion(hasUrl(vertices(), codeSystem.getUrl()), codeSystem.getVersion()) - .elementMap("count") - .tryNext(); + .elementMap("count") + .tryNext(); if (optional.isPresent()) { return (Integer) optional.get().get("count"); } From d7bdc20a58763ef89272c926a3ad6cb1fece9125 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 23 Mar 2021 12:03:12 -0400 Subject: [PATCH 46/50] Issue #1980 - additional argument checking Signed-off-by: John T.E. Timm --- .../provider/GraphTermServiceProvider.java | 42 +++++++++++++++---- 1 file changed, 33 insertions(+), 9 deletions(-) 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 0162c573374..5dce6d98885 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 @@ -97,7 +97,7 @@ public GraphTermServiceProvider(FHIRTermGraph graph, int timeLimit) { @SuppressWarnings("unchecked") @Override public Set closure(CodeSystem codeSystem, Code code) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + checkArguments(codeSystem, code); Set concepts = new LinkedHashSet<>(); @@ -121,13 +121,13 @@ public Set closure(CodeSystem codeSystem, Code code) { @Override public Concept getConcept(CodeSystem codeSystem, Code code) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + checkArguments(codeSystem, code); return getConcept(codeSystem, code, true, true); } @Override public Set getConcepts(CodeSystem codeSystem) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + checkArgument(codeSystem); Set concepts = new LinkedHashSet<>(getCount(codeSystem)); @@ -145,7 +145,7 @@ public Set getConcepts(CodeSystem codeSystem) { @Override public Set getConcepts(CodeSystem codeSystem, List filters) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + checkArguments(codeSystem, filters); Set concepts = new LinkedHashSet<>(); @@ -209,18 +209,19 @@ public int getTimeLimit() { @Override public boolean hasConcept(CodeSystem codeSystem, Code code) { + checkArguments(codeSystem, code); return whereCodeSystem(hasCode(vertices(), code.getValue(), isCaseSensitive(codeSystem)), codeSystem).hasNext(); } @Override public boolean isSupported(CodeSystem codeSystem) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + checkArgument(codeSystem); return hasVersion(hasUrl(vertices(), codeSystem.getUrl()), codeSystem.getVersion()).hasNext(); } @Override public boolean subsumes(CodeSystem codeSystem, Code codeA, Code codeB) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + checkArguments(codeSystem, codeA, codeB); boolean caseSensitive = isCaseSensitive(codeSystem); @@ -469,6 +470,32 @@ private GraphTraversal applyRegexFilter(CodeSystem codeSystem, F return filterNotApplied(filter, g); } + private void checkArgument(Code code, String message) { + Objects.requireNonNull(code, message); + Objects.requireNonNull(code.getValue(), "Code.value"); + } + + private void checkArgument(CodeSystem codeSystem) { + Objects.requireNonNull(codeSystem, "codeSystem"); + Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + } + + private void checkArguments(CodeSystem codeSystem, Code code) { + checkArgument(codeSystem); + checkArgument(code, "code"); + } + + private void checkArguments(CodeSystem codeSystem, Code codeA, Code codeB) { + checkArgument(codeSystem); + checkArgument(codeA, "codeA"); + checkArgument(codeB, "codeB"); + } + + private void checkArguments(CodeSystem codeSystem, List filters) { + checkArgument(codeSystem); + Objects.requireNonNull(filters, "filters"); + } + private void checkTimeLimit(TimeLimitStep timeLimitStep) { if (timeLimitStep.getTimedOut()) { throw new FHIRTermServiceException("Graph traversal timed out", Collections.singletonList(Issue.builder() @@ -529,7 +556,6 @@ private GraphTraversal filterNotApplied(Filter filter, GraphTrav } private Concept getConcept(CodeSystem codeSystem, Code code, boolean includeDesignations, boolean includeProperties) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); return createConcept( codeSystem, code.getValue(), @@ -551,7 +577,6 @@ private int getCount(CodeSystem codeSystem) { } private List getDesignations(CodeSystem codeSystem, String code) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); List designations = new ArrayList<>(); whereCodeSystem(hasCode(vertices(), code, isCaseSensitive(codeSystem)), codeSystem) .out("designation") @@ -562,7 +587,6 @@ private List getDesignations(CodeSystem codeSystem, String code) { } private List getProperties(CodeSystem codeSystem, String code) { - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); List properties = new ArrayList<>(); whereCodeSystem(hasCode(vertices(), code, isCaseSensitive(codeSystem)), codeSystem) .out("property_") From ae51b1ea2cab52584f71e9348ff8ce9d5fdd4b63 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 23 Mar 2021 13:01:31 -0400 Subject: [PATCH 47/50] Issue #1980 - formatting Signed-off-by: John T.E. Timm --- .../fhir/term/graph/provider/GraphTermServiceProvider.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 5dce6d98885..d66c921bdc6 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 @@ -551,7 +551,11 @@ private Property createProperty(Map elementMap) { } private GraphTraversal filterNotApplied(Filter filter, GraphTraversal g) { - log.log(Level.WARNING, String.format("Filter not applied - property: %s, op: %s, value: %s", filter.getProperty().getValue(), filter.getOp().getValue(), filter.getValue().getValue())); + log.log(Level.WARNING, + String.format("Filter not applied (property: %s, op: %s, value: %s)", + filter.getProperty().getValue(), + filter.getOp().getValue(), + filter.getValue().getValue())); return g; } From d8e3f7b6566a34f1cb046a670ef40eeebc589eeb Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 23 Mar 2021 13:17:24 -0400 Subject: [PATCH 48/50] Issue #1980 - more clean-up Signed-off-by: John T.E. Timm --- .../provider/GraphTermServiceProvider.java | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) 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 d66c921bdc6..d0457ad1f6c 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 @@ -15,6 +15,7 @@ import static com.ibm.fhir.term.util.CodeSystemSupport.isCaseSensitive; import static com.ibm.fhir.term.util.CodeSystemSupport.normalize; import static com.ibm.fhir.term.util.CodeSystemSupport.toElement; +import static java.util.Objects.requireNonNull; import java.util.ArrayList; import java.util.Arrays; @@ -22,7 +23,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.logging.Level; @@ -73,24 +73,24 @@ public class GraphTermServiceProvider implements FHIRTermServiceProvider { private final int timeLimit; public GraphTermServiceProvider(Configuration configuration) { - Objects.requireNonNull(configuration, "configuration"); + requireNonNull(configuration, "configuration"); graph = FHIRTermGraphFactory.open(configuration); timeLimit = DEFAULT_TIME_LIMIT; } public GraphTermServiceProvider(Configuration configuration, int timeLimit) { - Objects.requireNonNull(configuration, "configuration"); + requireNonNull(configuration, "configuration"); graph = FHIRTermGraphFactory.open(configuration); this.timeLimit = timeLimit; } public GraphTermServiceProvider(FHIRTermGraph graph) { - this.graph = Objects.requireNonNull(graph, "graph"); + this.graph = requireNonNull(graph, "graph"); timeLimit = DEFAULT_TIME_LIMIT; } public GraphTermServiceProvider(FHIRTermGraph graph, int timeLimit) { - this.graph = Objects.requireNonNull(graph, "graph"); + this.graph = requireNonNull(graph, "graph"); this.timeLimit = timeLimit; } @@ -471,13 +471,23 @@ private GraphTraversal applyRegexFilter(CodeSystem codeSystem, F } private void checkArgument(Code code, String message) { - Objects.requireNonNull(code, message); - Objects.requireNonNull(code.getValue(), "Code.value"); + requireNonNull(code, message); + requireNonNull(code.getValue(), "Code.value"); } private void checkArgument(CodeSystem codeSystem) { - Objects.requireNonNull(codeSystem, "codeSystem"); - Objects.requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + requireNonNull(codeSystem, "codeSystem"); + requireNonNull(codeSystem.getUrl(), "CodeSystem.url"); + } + + private void checkArgument(Filter filter) { + requireNonNull(filter, "filter"); + requireNonNull(filter.getProperty(), "Filter.property"); + requireNonNull(filter.getProperty().getValue(), "Filter.property.value"); + requireNonNull(filter.getOp(), "Filter.op"); + requireNonNull(filter.getOp().getValue(), "Filter.op.value"); + requireNonNull(filter.getValue(), "Filter.value"); + requireNonNull(filter.getValue().getValue(), "Filter.value.value"); } private void checkArguments(CodeSystem codeSystem, Code code) { @@ -493,7 +503,8 @@ private void checkArguments(CodeSystem codeSystem, Code codeA, Code codeB) { private void checkArguments(CodeSystem codeSystem, List filters) { checkArgument(codeSystem); - Objects.requireNonNull(filters, "filters"); + requireNonNull(filters, "filters"); + filters.forEach(filter -> checkArgument(filter)); } private void checkTimeLimit(TimeLimitStep timeLimitStep) { From f164092268bca7f5f9d17c5cfcb73501dff6df79 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 23 Mar 2021 13:19:25 -0400 Subject: [PATCH 49/50] Issue #1980 - removed commented code Signed-off-by: John T.E. Timm --- .../fhir/term/graph/provider/GraphTermServiceProvider.java | 6 ------ 1 file changed, 6 deletions(-) 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 d0457ad1f6c..a2be2251d28 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 @@ -367,12 +367,6 @@ private GraphTraversal applyIsNotAFilter(CodeSystem codeSystem, Code property = filter.getProperty(); com.ibm.fhir.model.type.String value = filter.getValue(); if ("concept".equals(property.getValue()) && CodeSystemHierarchyMeaning.IS_A.equals(codeSystem.getHierarchyMeaning())) { - /* - // alternative from: https://groups.google.com/g/gremlin-users/c/hHlVRMl1JZs - return whereCodeSystem(g.not(__.until(hasCode(value.getValue(), caseSensitive)) - .repeat((GraphTraversal) __.out(FHIRTermGraph.IS_A))) - .hasLabel("Concept"), codeSystem); - */ return whereCodeSystem(g.not(__.repeat(__.out(FHIRTermGraph.IS_A)) .until(hasCode(value.getValue(), caseSensitive))) .not(hasCode(value.getValue(), caseSensitive)) From ed1d16b19633eb1b1b3cd1a68abe117cfbd5a0e3 Mon Sep 17 00:00:00 2001 From: "John T.E. Timm" Date: Tue, 23 Mar 2021 13:23:52 -0400 Subject: [PATCH 50/50] Issue #1980 - formatting Signed-off-by: John T.E. Timm --- .../fhir/term/graph/provider/GraphTermServiceProvider.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 a2be2251d28..40178f9f5f2 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 @@ -302,8 +302,8 @@ private GraphTraversal applyEqualsFilter(CodeSystem codeSystem, return "parent".equals(property.getValue()) ? applyParentEqualsFilter(codeSystem, filter, first, g) : "child".equals(property.getValue()) ? - applyChildEqualsFilter(codeSystem, filter, first, g) : - applyPropertyEqualsFilter(codeSystem, filter, first, g); + applyChildEqualsFilter(codeSystem, filter, first, g) : + applyPropertyEqualsFilter(codeSystem, filter, first, g); } return filterNotApplied(filter, g); }