diff --git a/build.gradle b/build.gradle index b6403f6c2..d263caf57 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { ext { - springBootVersion = '3.4.12' + springBootVersion = '3.5.13' // Remove this line to see what version is favored by springBootVersion // and the version brought in by spring-data-elasticsearch set('elasticsearch.version', '7.12.1') @@ -87,26 +87,23 @@ dependencies { implementation "org.springframework.boot:spring-boot-devtools" implementation "org.springframework.boot:spring-boot-starter-actuator" - // Explicit patch for spring-core to mitigate CVE-2025-41249 - implementation 'org.springframework:spring-core:6.2.11' implementation "org.springframework.boot:spring-boot-starter-cache" implementation "org.springframework.boot:spring-boot-properties-migrator" implementation "org.springframework:spring-aop" - // For vulnerabilities - may not be needed with Spring Boot 3.4.10 - // implementation "org.apache.tomcat.embed:tomcat-embed-core:10.1.44" - implementation "com.fasterxml.jackson.core:jackson-core:2.18.6" + // These override org.hl7.fhir.*:6.4.0 requested by hapi-fhir-validation:7.6.1 and hapi-fhir-converter:7.6.1. + // 6.4.0 contains a vulnerability; 6.9.4 is the version co-packaged with HAPI FHIR that fixes it. + // These overrides can only be removed by upgrading to HAPI FHIR 8.x (a major breaking-change migration). implementation "org.fhir:ucum:1.0.9" - // These are needed to override dependencies from hapi-fhir-base (until it is fixed) - implementation "ca.uhn.hapi.fhir:org.hl7.fhir.convertors:6.9.0" - implementation "ca.uhn.hapi.fhir:org.hl7.fhir.dstu2:6.9.0" - implementation "ca.uhn.hapi.fhir:org.hl7.fhir.dstu2016may:6.9.0" - implementation "ca.uhn.hapi.fhir:org.hl7.fhir.dstu3:6.9.0" - implementation "ca.uhn.hapi.fhir:org.hl7.fhir.r4:6.9.0" - implementation "ca.uhn.hapi.fhir:org.hl7.fhir.r4b:6.9.0" - implementation "ca.uhn.hapi.fhir:org.hl7.fhir.r5:6.9.0" - implementation "ca.uhn.hapi.fhir:org.hl7.fhir.utilities:6.9.0" - implementation "ca.uhn.hapi.fhir:org.hl7.fhir.validation:6.9.0" + implementation "ca.uhn.hapi.fhir:org.hl7.fhir.convertors:6.9.4" + implementation "ca.uhn.hapi.fhir:org.hl7.fhir.dstu2:6.9.4" + implementation "ca.uhn.hapi.fhir:org.hl7.fhir.dstu2016may:6.9.4" + implementation "ca.uhn.hapi.fhir:org.hl7.fhir.dstu3:6.9.4" + implementation "ca.uhn.hapi.fhir:org.hl7.fhir.r4:6.9.4" + implementation "ca.uhn.hapi.fhir:org.hl7.fhir.r4b:6.9.4" + implementation "ca.uhn.hapi.fhir:org.hl7.fhir.r5:6.9.4" + implementation "ca.uhn.hapi.fhir:org.hl7.fhir.utilities:6.9.4" + implementation "ca.uhn.hapi.fhir:org.hl7.fhir.validation:6.9.4" //implementation "org.springframework.data:spring-data-elasticsearch:4.2.12" implementation "org.opensearch.client:spring-data-opensearch-starter:1.7.1" @@ -118,14 +115,15 @@ dependencies { //Java Mail Sender dependency implementation "org.springframework.boot:spring-boot-starter-mail" - implementation "org.aspectj:aspectjweaver:1.9.2" + // aspectjweaver is now managed by Spring Boot BOM (1.9.25.1 in 3.5.x) + implementation "org.aspectj:aspectjweaver" implementation "org.apache.commons:commons-text:1.10.0" //implementation "org.apache.opennlp:opennlp-tools:2.2.0" // Upgrade ecache to use jakarta - https://groups.google.com/g/ehcache-users/c/sKfxWuTpY-U // See also: https://stackoverflow.com/questions/75813659/gradle-could-not-find-org-ehcacheehcache-after-upgrading-to-spring-boot-3-0-x // Note: ehcache 3.10.8 uses Jakarta by default, no classifier needed - implementation "org.ehcache:ehcache:3.10.8" + implementation "org.ehcache:ehcache:3.10.9" // JAXB dependencies required for ehcache with Java 9+ // ehcache 3.10.8 still uses javax.xml.bind internally for XML parsing diff --git a/src/main/java/gov/nih/nci/evs/api/fhir/R4/CodeSystemProviderR4.java b/src/main/java/gov/nih/nci/evs/api/fhir/R4/CodeSystemProviderR4.java index 509d56995..4cd295c59 100644 --- a/src/main/java/gov/nih/nci/evs/api/fhir/R4/CodeSystemProviderR4.java +++ b/src/main/java/gov/nih/nci/evs/api/fhir/R4/CodeSystemProviderR4.java @@ -141,6 +141,7 @@ public Parameters lookupImplicit( } else if (coding != null) { codeToLookup = coding.getCode(); } + // This should be the latest (+monthly) version final CodeSystem codeSys = cs.get(0); final Terminology term = termUtils.getIndexedTerminology(codeSys.getTitle(), osQueryService, true); @@ -296,6 +297,7 @@ public Parameters lookupInstance( } else if (coding != null) { codeToLookup = coding.getCode(); } + // This should be the latest (+monthly) version final CodeSystem codeSys = cs.get(0); // if system is supplied, ensure it matches the url returned on the codeSys found by id if ((systemToLookup != null) && !codeSys.getUrl().equals(systemToLookup.getValue())) { @@ -467,6 +469,7 @@ public Parameters validateCodeImplicit( } else if (coding != null) { codeToValidate = coding.getCode(); } + // This should be the latest (+monthly) version final CodeSystem codeSys = cs.get(0); final Terminology term = termUtils.getIndexedTerminology(codeSys.getTitle(), osQueryService, true); @@ -583,6 +586,7 @@ public Parameters validateCodeInstance( } else if (coding != null) { codeToValidate = coding.getCode(); } + // This should be the latest (+monthly) version final CodeSystem codeSys = cs.get(0); // if url is supplied, ensure it matches the url returned on the codeSys found by id if ((systemToLookup != null) && !codeSys.getUrl().equals(systemToLookup.getValue())) { @@ -707,6 +711,7 @@ public Parameters subsumesImplicit( throw FhirUtilityR4.exception( "No codeB parameter provided in request", OperationOutcome.IssueType.EXCEPTION, 400); } + // This should be the latest (+monthly) version final CodeSystem codeSys = cs.get(0); final Terminology term = termUtils.getIndexedTerminology(codeSys.getTitle(), osQueryService, true); @@ -809,6 +814,7 @@ public Parameters subsumesInstance( throw FhirUtilityR4.exception( "No codeB parameter provided in request", OperationOutcome.IssueType.EXCEPTION, 400); } + // This should be the latest (+monthly) version final CodeSystem codeSys = cs.get(0); final Terminology term = termUtils.getIndexedTerminology(codeSys.getTitle(), osQueryService, true); @@ -880,11 +886,9 @@ public Bundle findCodeSystems( FhirUtilityR4.notSupportedSearchParams(request); FhirUtilityR4.mutuallyExclusive("url", url, "system", system); - final List terms = termUtils.getIndexedTerminologies(osQueryService); - final List list = new ArrayList<>(); - for (final Terminology terminology : terms) { - final CodeSystem cs = FhirUtilityR4.toR4(terminology); + for (final CodeSystem cs : findPossibleCodeSystems(null, null, null)) { + // Skip non-matching if ((id != null && !id.getValue().equals(cs.getId())) || (system != null && !system.getValue().equals(cs.getUrl()))) { @@ -911,7 +915,7 @@ public Bundle findCodeSystems( list.add(cs); } - // Apply sorting if requested + // Apply sorting if requested via API applySorting(list, sort); return FhirUtilityR4.makeBundle(request, list, count, offset); @@ -935,17 +939,16 @@ public Bundle findCodeSystems( * @throws Exception the exception */ private List findPossibleCodeSystems( - @OptionalParam(name = "_id") final IdType id, - @OptionalParam(name = "url") final UriType url, - @OptionalParam(name = "version") final StringType version) - throws Exception { + final IdType id, final UriType url, final StringType version) throws Exception { try { - // If no ID and no url are specified, no code systems match - if (id == null && url == null) { - return new ArrayList<>(0); - } + // If no ID and no url are specified, ALL code systems match + // if (id == null && url == null) { + // return new ArrayList<>(0); + // } + // Get all terminologies sorted on version final List terms = termUtils.getIndexedTerminologies(osQueryService); + Collections.sort(terms, TerminologyUtils.SORT_LATEST_MONTHLY); final List list = new ArrayList<>(); for (final Terminology terminology : terms) { @@ -963,6 +966,7 @@ private List findPossibleCodeSystems( list.add(cs); } + return list; } catch (final FHIRServerResponseException e) { throw e; diff --git a/src/main/java/gov/nih/nci/evs/api/fhir/R4/ConceptMapProviderR4.java b/src/main/java/gov/nih/nci/evs/api/fhir/R4/ConceptMapProviderR4.java index f798df3a8..c92ca7210 100644 --- a/src/main/java/gov/nih/nci/evs/api/fhir/R4/ConceptMapProviderR4.java +++ b/src/main/java/gov/nih/nci/evs/api/fhir/R4/ConceptMapProviderR4.java @@ -502,6 +502,7 @@ private List findPossibleConceptMaps( map.put(terminology.getTerminology(), terminology); } final List mapsets = osQueryService.getMapsets(new IncludeParam("properties")); + Collections.sort(mapsets, TerminologyUtils.REVERSE_SORT_VERSIONS); final List list = new ArrayList<>(); for (final Concept mapset : mapsets) { @@ -554,6 +555,7 @@ private List findPossibleConceptMaps( list.add(cm); } + return list; } catch (final FHIRServerResponseException e) { throw e; diff --git a/src/main/java/gov/nih/nci/evs/api/fhir/R4/ValueSetProviderR4.java b/src/main/java/gov/nih/nci/evs/api/fhir/R4/ValueSetProviderR4.java index d58bd7c26..212b7d7ad 100644 --- a/src/main/java/gov/nih/nci/evs/api/fhir/R4/ValueSetProviderR4.java +++ b/src/main/java/gov/nih/nci/evs/api/fhir/R4/ValueSetProviderR4.java @@ -1638,10 +1638,7 @@ public ValueSet getValueSet(@IdParam final IdType id) throws Exception { * @throws Exception the exception */ private List findPossibleValueSets( - @OptionalParam(name = "_id") final IdType id, - @OptionalParam(name = "system") final UriType system, - @OptionalParam(name = "url") final UriType url, - @OptionalParam(name = "version") final StringType version) + final IdType id, final UriType system, final UriType url, final StringType version) throws Exception { // If no ID and no url are specified, no code systems match if (id == null && url == null) { @@ -1649,6 +1646,7 @@ private List findPossibleValueSets( } final List terms = termUtils.getIndexedTerminologies(osQueryService); + Collections.sort(terms, TerminologyUtils.SORT_LATEST_MONTHLY); final Map map = new HashMap<>(); final List list = new ArrayList(); @@ -1675,6 +1673,8 @@ private List findPossibleValueSets( list.add(vs); } + + // This currently only gets latest monthly subsets, not earlier versions final List subsets = getNcitSubsets(); final List subsetsAsConcepts = subsets.stream().flatMap(Concept::streamSelfAndChildren).toList(); diff --git a/src/main/java/gov/nih/nci/evs/api/fhir/R5/CodeSystemProviderR5.java b/src/main/java/gov/nih/nci/evs/api/fhir/R5/CodeSystemProviderR5.java index 5d4d430a0..7681a155d 100644 --- a/src/main/java/gov/nih/nci/evs/api/fhir/R5/CodeSystemProviderR5.java +++ b/src/main/java/gov/nih/nci/evs/api/fhir/R5/CodeSystemProviderR5.java @@ -122,12 +122,10 @@ public Bundle findCodeSystems( FhirUtilityR5.mutuallyExclusive("url", url, "system", system); // Get the indexed terms - final List terms = termUtils.getIndexedTerminologies(osQueryService); final List list = new ArrayList<>(); // Find the matching code systems in the list of terms - for (final Terminology terminology : terms) { - final CodeSystem cs = FhirUtilityR5.toR5(terminology); + for (final CodeSystem cs : findPossibleCodeSystems(null, null, null)) { // Skip non-matching if ((id != null && !id.getValue().equals(cs.getIdPart())) @@ -246,6 +244,7 @@ public Parameters lookupImplicit( } else if (coding != null) { codeToLookup = coding.getCode(); } + // This should be the latest (+monthly) version final CodeSystem codeSys = cs.get(0); final Terminology term = termUtils.getIndexedTerminology(codeSys.getTitle(), osQueryService, true); @@ -399,6 +398,7 @@ public Parameters lookupInstance( } else if (coding != null) { codeToLookup = coding.getCode(); } + // This should be the latest (+monthly) version final CodeSystem codeSys = cs.get(0); if ((systemToLookup != null) && !codeSys.getUrl().equals(systemToLookup.getValue())) { throw FhirUtilityR5.exception( @@ -989,16 +989,16 @@ public CodeSystem getCodeSystem(@IdParam final IdType id) throws Exception { * @throws Exception exception */ private List findPossibleCodeSystems( - @OperationParam(name = "_id") final IdType id, - @OperationParam(name = "url") final UriType url, - @OperationParam(name = "version") final StringType version) - throws Exception { + final IdType id, final UriType url, final StringType version) throws Exception { try { - if (id == null && url == null) { - return new ArrayList<>(0); - } + // If no ID and no url are specified, ALL code systems match + // if (id == null && url == null) { + // return new ArrayList<>(0); + // } final List terms = termUtils.getIndexedTerminologies(osQueryService); + Collections.sort(terms, TerminologyUtils.SORT_LATEST_MONTHLY); + final List list = new ArrayList<>(); // Find the matching code systems for (final Terminology terminology : terms) { @@ -1015,6 +1015,7 @@ private List findPossibleCodeSystems( } list.add(cs); } + return list; } catch (final FHIRServerResponseException e) { throw e; diff --git a/src/main/java/gov/nih/nci/evs/api/fhir/R5/ConceptMapProviderR5.java b/src/main/java/gov/nih/nci/evs/api/fhir/R5/ConceptMapProviderR5.java index d772f070f..3047c6140 100644 --- a/src/main/java/gov/nih/nci/evs/api/fhir/R5/ConceptMapProviderR5.java +++ b/src/main/java/gov/nih/nci/evs/api/fhir/R5/ConceptMapProviderR5.java @@ -588,6 +588,7 @@ private List findPossibleConceptMaps( map.put(terminology.getTerminology(), terminology); } final List mapsets = osQueryService.getMapsets(new IncludeParam("properties")); + Collections.sort(mapsets, TerminologyUtils.REVERSE_SORT_VERSIONS); final List list = new ArrayList<>(); // Find the matching mapsets @@ -633,6 +634,7 @@ private List findPossibleConceptMaps( list.add(cm); } + return list; } catch (final FHIRServerResponseException e) { throw e; diff --git a/src/main/java/gov/nih/nci/evs/api/fhir/R5/OpenApiInterceptorR5.java b/src/main/java/gov/nih/nci/evs/api/fhir/R5/OpenApiInterceptorR5.java index f9d8ef5d5..eb105c12a 100644 --- a/src/main/java/gov/nih/nci/evs/api/fhir/R5/OpenApiInterceptorR5.java +++ b/src/main/java/gov/nih/nci/evs/api/fhir/R5/OpenApiInterceptorR5.java @@ -932,6 +932,7 @@ private CapabilityStatement getCapabilityStatement( * @param theResourceType the resource type * @param theOperation the operation */ + @SuppressWarnings("null") private void addFhirOperation( final FhirContext theFhirContext, final OpenAPI theOpenApi, diff --git a/src/main/java/gov/nih/nci/evs/api/fhir/R5/ValueSetProviderR5.java b/src/main/java/gov/nih/nci/evs/api/fhir/R5/ValueSetProviderR5.java index 218691967..4426573c7 100644 --- a/src/main/java/gov/nih/nci/evs/api/fhir/R5/ValueSetProviderR5.java +++ b/src/main/java/gov/nih/nci/evs/api/fhir/R5/ValueSetProviderR5.java @@ -1259,10 +1259,7 @@ public ValueSet getValueSet(@IdParam final IdType id) throws Exception { * @throws Exception the exception */ public List findPossibleValueSets( - @OptionalParam(name = "_id") final IdType id, - @OptionalParam(name = "system") final UriType system, - @OptionalParam(name = "url") final UriType url, - @OptionalParam(name = "version") final StringType version) + final IdType id, final UriType system, final UriType url, final StringType version) throws Exception { // If no ID and no url are specified, no code systems match if (id == null && url == null) { @@ -1270,6 +1267,8 @@ public List findPossibleValueSets( } final List terms = termUtils.getIndexedTerminologies(osQueryService); + Collections.sort(terms, TerminologyUtils.SORT_LATEST_MONTHLY); + final List list = new ArrayList(); final Map map = new HashMap<>(); for (final Terminology terminology : terms) { @@ -1294,6 +1293,7 @@ public List findPossibleValueSets( } list.add(vs); } + // This currently only gets latest monthly subsets, not earlier versions final List subsets = getNcitSubsets(); final Set subsetsAsConcepts = subsets.stream().flatMap(Concept::streamSelfAndChildren).collect(Collectors.toSet()); @@ -1315,6 +1315,7 @@ public List findPossibleValueSets( } list.add(vs); } + return list; } diff --git a/src/main/java/gov/nih/nci/evs/api/service/LoaderServiceImpl.java b/src/main/java/gov/nih/nci/evs/api/service/LoaderServiceImpl.java index 53b349fbe..e0e2b15ff 100644 --- a/src/main/java/gov/nih/nci/evs/api/service/LoaderServiceImpl.java +++ b/src/main/java/gov/nih/nci/evs/api/service/LoaderServiceImpl.java @@ -72,7 +72,6 @@ public static void setStaticServices( } @PostConstruct - @SuppressWarnings("static-access") public void init() { setStaticServices(this.operationsService, this.osQueryService, this.termUtils); } diff --git a/src/main/java/gov/nih/nci/evs/api/service/OpenSearchServiceImpl.java b/src/main/java/gov/nih/nci/evs/api/service/OpenSearchServiceImpl.java index 70fafccd3..1f6415017 100644 --- a/src/main/java/gov/nih/nci/evs/api/service/OpenSearchServiceImpl.java +++ b/src/main/java/gov/nih/nci/evs/api/service/OpenSearchServiceImpl.java @@ -129,7 +129,8 @@ public ConceptResultList findConcepts( .withQuery(boolQuery) .withPageable(pageable) .withSourceFilter( - new FetchSourceFilter(include.getIncludedFields(), include.getExcludedFields())); + new FetchSourceFilter( + true, include.getIncludedFields(), include.getExcludedFields())); // avoid setting min score // .withMinScore(0.01f); diff --git a/src/main/java/gov/nih/nci/evs/api/service/OpensearchQueryServiceImpl.java b/src/main/java/gov/nih/nci/evs/api/service/OpensearchQueryServiceImpl.java index 0c589d726..31c59011d 100644 --- a/src/main/java/gov/nih/nci/evs/api/service/OpensearchQueryServiceImpl.java +++ b/src/main/java/gov/nih/nci/evs/api/service/OpensearchQueryServiceImpl.java @@ -119,7 +119,8 @@ public List getConcepts( NativeSearchQuery query = new NativeSearchQueryBuilder() .withFilter(QueryBuilders.idsQuery().addIds(codes.toArray(new String[0]))) - .withSourceFilter(new FetchSourceFilter(ip.getIncludedFields(), ip.getExcludedFields())) + .withSourceFilter( + new FetchSourceFilter(true, ip.getIncludedFields(), ip.getExcludedFields())) .withPageable(new EVSPageable(0, codes.size(), 0)) .build(); @@ -257,7 +258,9 @@ public List getSuperclasses(String code, Terminology terminology) { @Override public Optional getLabel(String code, Terminology terminology) { Optional concept = getConcept(code, terminology, new IncludeParam("minimal")); - if (!concept.isPresent() || concept.get().getName() == null) return Optional.empty(); + if (!concept.isPresent() || concept.get().getName() == null) { + return Optional.empty(); + } return Optional.of(concept.get().getName()); } @@ -275,7 +278,9 @@ public Optional getLabel(String code, Terminology terminology) { public List getRootNodes(Terminology terminology, IncludeParam ip) throws JsonParseException, JsonMappingException, IOException { Optional hierarchy = getHierarchyRoots(terminology); - if (!hierarchy.isPresent()) return Collections.emptyList(); + if (!hierarchy.isPresent()) { + return Collections.emptyList(); + } List hierarchyRoots = hierarchy.get().getHierarchyRoots(); if (hierarchyRoots.size() < 1) { return new ArrayList(); @@ -302,7 +307,9 @@ public List getRootNodes(Terminology terminology, IncludeParam ip) public List getRootNodesHierarchy(Terminology terminology) throws JsonParseException, JsonMappingException, IOException { Optional hierarchy = getHierarchyRoots(terminology); - if (!hierarchy.isPresent()) return Collections.emptyList(); + if (!hierarchy.isPresent()) { + return Collections.emptyList(); + } List nodes = new ArrayList<>(); List hierarchyRoots = hierarchy.get().getHierarchyRoots(); List concepts = getConcepts(hierarchyRoots, terminology, new IncludeParam("minimal")); @@ -515,7 +522,9 @@ public List getIndexMetadata(boolean completedOnly) { public Optional getHierarchyRoots(Terminology terminology) throws JsonMappingException, JsonProcessingException { Optional esObject = getOpensearchObject("hierarchy", terminology); - if (!esObject.isPresent()) return Optional.empty(); + if (!esObject.isPresent()) { + return Optional.empty(); + } return Optional.of(esObject.get().getHierarchy()); } @@ -871,7 +880,8 @@ public List getMapsets(IncludeParam ip) throws Exception { NativeSearchQuery query = new NativeSearchQueryBuilder() - .withSourceFilter(new FetchSourceFilter(ip.getIncludedFields(), ip.getExcludedFields())) + .withSourceFilter( + new FetchSourceFilter(true, ip.getIncludedFields(), ip.getExcludedFields())) // assuming pageSize < 10000, trying to get all maps, 17 at the time of this comment .withPageable(PageRequest.of(0, 10000)) .build(); @@ -885,7 +895,8 @@ public List getMapset(String code, IncludeParam ip) throws Exception { NativeSearchQuery query = new NativeSearchQueryBuilder() .withFilter(QueryBuilders.termQuery("_id", code)) - .withSourceFilter(new FetchSourceFilter(ip.getIncludedFields(), ip.getExcludedFields())) + .withSourceFilter( + new FetchSourceFilter(true, ip.getIncludedFields(), ip.getExcludedFields())) .build(); return getResults(query, Concept.class, OpensearchOperationsService.MAPSET_INDEX); diff --git a/src/main/java/gov/nih/nci/evs/api/service/SparqlQueryManagerServiceImpl.java b/src/main/java/gov/nih/nci/evs/api/service/SparqlQueryManagerServiceImpl.java index b81f291ba..d465c7442 100644 --- a/src/main/java/gov/nih/nci/evs/api/service/SparqlQueryManagerServiceImpl.java +++ b/src/main/java/gov/nih/nci/evs/api/service/SparqlQueryManagerServiceImpl.java @@ -2228,7 +2228,6 @@ public List getAllRoles(final Terminology terminology, final IncludePar .orElse(null); } if (concept.getCode().equals(concept.getName()) - && bindings != null && matchConcept != null && matchConcept.getPropertyLabel() != null) { concept.setName(matchConcept.getPropertyLabel().getValue()); diff --git a/src/main/java/gov/nih/nci/evs/api/util/TerminologyUtils.java b/src/main/java/gov/nih/nci/evs/api/util/TerminologyUtils.java index 04e86898d..3837636b7 100644 --- a/src/main/java/gov/nih/nci/evs/api/util/TerminologyUtils.java +++ b/src/main/java/gov/nih/nci/evs/api/util/TerminologyUtils.java @@ -1,5 +1,6 @@ package gov.nih.nci.evs.api.util; +import gov.nih.nci.evs.api.model.Concept; import gov.nih.nci.evs.api.model.Terminology; import gov.nih.nci.evs.api.properties.ApplicationProperties; import gov.nih.nci.evs.api.properties.GraphProperties; @@ -11,6 +12,7 @@ import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -43,6 +45,42 @@ public final class TerminologyUtils { /** The Constant logger. */ private static final Logger logger = LoggerFactory.getLogger(TerminologyUtils.class); + /** The Constant SORT_LATEST. Latest sorts before not latest. */ + public static final Comparator SORT_LATEST = + new Comparator<>() { + @Override + public int compare(final Terminology t1, final Terminology t2) { + return Boolean.compare(t1.getLatest(), t2.getLatest()); + } + }; + + /** + * The Constant SORT_LATEST_MONTHLY. Monthly sorts before weekly. latest sorts before not latest. + * (lowest string value sorts first) + */ + public static final Comparator SORT_LATEST_MONTHLY = + new Comparator<>() { + @Override + public int compare(final Terminology t1, final Terminology t2) { + final String k1 = + (t1.getTags().containsKey("monthly") ? "0" : "1") + (t1.getLatest() ? "0" : "1"); + final String k2 = + (t2.getTags().containsKey("monthly") ? "0" : "1") + (t2.getLatest() ? "0" : "1"); + return k1.compareTo(k2); + } + }; + + /** The Constant SORT_VERSIONS. Reverse sort based on version (+terminology) alphabetically. */ + public static final Comparator REVERSE_SORT_VERSIONS = + new Comparator<>() { + @Override + public int compare(final Concept c1, final Concept c2) { + final String k1 = c1.getVersion() + c1.getTerminology(); + final String k2 = c1.getVersion() + c1.getTerminology(); + return k2.compareTo(k1); + } + }; + /** The graph db properties. */ @Autowired GraphProperties graphProperties; @@ -92,7 +130,8 @@ public List getIndexedTerminologies(OpensearchQueryService osQueryS * Returns the stale terminologies. * * @param dbs the dbs - * @param terminology the terminology + * @param sparqlQueryManagerService the sparql query manager service + * @param osQueryService the os query service * @return the stale terminologies * @throws Exception the exception */ @@ -143,10 +182,11 @@ public Terminology getTerminology( } /** - * Get the indexed terminology + * Get the indexed terminology. * * @param terminology search terminology * @param osQueryService opensearch query service + * @param requireFlag the require flag * @return the Terminology * @throws Exception the exception */ @@ -162,6 +202,7 @@ public Terminology getIndexedTerminology( * * @param terminology target terminology * @param terminologies list of terminologies to search through + * @param requireFlag the require flag * @return the Terminology */ private Terminology findTerminology( diff --git a/src/test/java/gov/nih/nci/evs/api/fhir/FhirR4ValueSetReadSearchTests.java b/src/test/java/gov/nih/nci/evs/api/fhir/FhirR4ValueSetReadSearchTests.java index fe3a2bb71..47dd8a826 100644 --- a/src/test/java/gov/nih/nci/evs/api/fhir/FhirR4ValueSetReadSearchTests.java +++ b/src/test/java/gov/nih/nci/evs/api/fhir/FhirR4ValueSetReadSearchTests.java @@ -669,13 +669,14 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { data.getEntry().stream().map(BundleEntryComponent::getResource).toList(); final ValueSet firstValueSet = (ValueSet) valueSets.get(0); + final String firstValueSetName = firstValueSet.getName(); final String firstValueSetTitle = firstValueSet.getTitle(); // Test 2: Basic name search (without modifier) final String basicNameUrl = endpoint + "?name=" + URLEncoder.encode(firstValueSetName, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(basicNameUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(basicNameUrl), String.class); final Bundle basicNameBundle = parser.parseResource(Bundle.class, content); assertNotNull(basicNameBundle.getEntry()); @@ -687,7 +688,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String upperCaseName = firstValueSetName.toUpperCase(); final String exactMatchUrl = endpoint + "?name:exact=" + URLEncoder.encode(upperCaseName, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(exactMatchUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(exactMatchUrl), String.class); final Bundle exactMatchBundle = parser.parseResource(Bundle.class, content); assertNotNull(exactMatchBundle.getEntry()); @@ -699,7 +700,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String partialName = firstValueSetName.substring(1, firstValueSetName.length() - 1); String containsUrl = endpoint + "?name:contains=" + URLEncoder.encode(partialName, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(containsUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(containsUrl), String.class); Bundle containsBundle = parser.parseResource(Bundle.class, content); assertNotNull(containsBundle.getEntry()); @@ -714,7 +715,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String namePrefix = firstValueSetName.substring(0, 3); final String startsWithUrl = endpoint + "?name:startsWith=" + URLEncoder.encode(namePrefix, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(startsWithUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(startsWithUrl), String.class); final Bundle startsWithBundle = parser.parseResource(Bundle.class, content); assertNotNull(startsWithBundle.getEntry()); @@ -729,7 +730,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String nonExistentName = "NonExistentValueSet" + UUID.randomUUID(); final String negativeTestUrl = endpoint + "?name:exact=" + URLEncoder.encode(nonExistentName, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(negativeTestUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(negativeTestUrl), String.class); final Bundle emptyBundle = parser.parseResource(Bundle.class, content); assertTrue(emptyBundle.getEntry() == null || emptyBundle.getEntry().isEmpty()); @@ -738,7 +739,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String upperCaseTitle = firstValueSetTitle.toUpperCase(); final String titleExactUrl = endpoint + "?title:exact=" + URLEncoder.encode(upperCaseTitle, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(titleExactUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(titleExactUrl), String.class); final Bundle titleExactBundle = parser.parseResource(Bundle.class, content); assertNotNull(titleExactBundle.getEntry()); @@ -750,7 +751,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String titlePrefix = firstValueSetTitle.substring(0, 3); final String titleStartsWithUrl = endpoint + "?title:startsWith=" + URLEncoder.encode(titlePrefix, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(titleStartsWithUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(titleStartsWithUrl), String.class); final Bundle titleStartsWithBundle = parser.parseResource(Bundle.class, content); assertNotNull(titleStartsWithBundle.getEntry()); @@ -765,7 +766,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String partialTitle = firstValueSetTitle.substring(1, firstValueSetTitle.length() - 1); final String titleContainsUrl = endpoint + "?title:contains=" + URLEncoder.encode(partialTitle, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(titleContainsUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(titleContainsUrl), String.class); final Bundle titleContainsBundle = parser.parseResource(Bundle.class, content); assertNotNull(titleContainsBundle.getEntry()); @@ -794,7 +795,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String cdiscName = "CDISC"; containsUrl = endpoint + "?name:contains=" + URLEncoder.encode(cdiscName, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(containsUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(containsUrl), String.class); containsBundle = parser.parseResource(Bundle.class, content); assertNotNull(containsBundle.getEntry()); diff --git a/src/test/java/gov/nih/nci/evs/api/fhir/FhirR5ValueSetReadSearchTests.java b/src/test/java/gov/nih/nci/evs/api/fhir/FhirR5ValueSetReadSearchTests.java index fe120025c..adbfffb1b 100644 --- a/src/test/java/gov/nih/nci/evs/api/fhir/FhirR5ValueSetReadSearchTests.java +++ b/src/test/java/gov/nih/nci/evs/api/fhir/FhirR5ValueSetReadSearchTests.java @@ -707,7 +707,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { // Test 2: Basic name search (without modifier) final String basicNameUrl = endpoint + "?name=" + URLEncoder.encode(firstValueSetName, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(basicNameUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(basicNameUrl), String.class); final Bundle basicNameBundle = parser.parseResource(Bundle.class, content); assertNotNull(basicNameBundle.getEntry()); @@ -719,7 +719,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String upperCaseName = firstValueSetName.toUpperCase(); final String exactMatchUrl = endpoint + "?name:exact=" + URLEncoder.encode(upperCaseName, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(exactMatchUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(exactMatchUrl), String.class); final Bundle exactMatchBundle = parser.parseResource(Bundle.class, content); assertNotNull(exactMatchBundle.getEntry()); @@ -731,7 +731,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String partialName = firstValueSetName.substring(1, firstValueSetName.length() - 1); String containsUrl = endpoint + "?name:contains=" + URLEncoder.encode(partialName, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(containsUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(containsUrl), String.class); Bundle containsBundle = parser.parseResource(Bundle.class, content); assertNotNull(containsBundle.getEntry()); @@ -746,7 +746,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String namePrefix = firstValueSetName.substring(0, 3); final String startsWithUrl = endpoint + "?name:startsWith=" + URLEncoder.encode(namePrefix, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(startsWithUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(startsWithUrl), String.class); final Bundle startsWithBundle = parser.parseResource(Bundle.class, content); assertNotNull(startsWithBundle.getEntry()); @@ -761,7 +761,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String nonExistentName = "NonExistentValueSet" + UUID.randomUUID(); final String negativeTestUrl = endpoint + "?name:exact=" + URLEncoder.encode(nonExistentName, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(negativeTestUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(negativeTestUrl), String.class); final Bundle emptyBundle = parser.parseResource(Bundle.class, content); assertTrue(emptyBundle.getEntry() == null || emptyBundle.getEntry().isEmpty()); @@ -770,7 +770,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String upperCaseTitle = firstValueSetTitle.toUpperCase(); final String titleExactUrl = endpoint + "?title:exact=" + URLEncoder.encode(upperCaseTitle, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(titleExactUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(titleExactUrl), String.class); final Bundle titleExactBundle = parser.parseResource(Bundle.class, content); assertNotNull(titleExactBundle.getEntry()); @@ -782,7 +782,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String titlePrefix = firstValueSetTitle.substring(0, 3); final String titleStartsWithUrl = endpoint + "?title:startsWith=" + URLEncoder.encode(titlePrefix, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(titleStartsWithUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(titleStartsWithUrl), String.class); final Bundle titleStartsWithBundle = parser.parseResource(Bundle.class, content); assertNotNull(titleStartsWithBundle.getEntry()); @@ -797,7 +797,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String partialTitle = firstValueSetTitle.substring(1, firstValueSetTitle.length() - 1); final String titleContainsUrl = endpoint + "?title:contains=" + URLEncoder.encode(partialTitle, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(titleContainsUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(titleContainsUrl), String.class); final Bundle titleContainsBundle = parser.parseResource(Bundle.class, content); assertNotNull(titleContainsBundle.getEntry()); @@ -826,7 +826,7 @@ public void testValueSetSearchVariantsWithParameters() throws Exception { final String cdiscName = "CDISC"; containsUrl = endpoint + "?name:contains=" + URLEncoder.encode(cdiscName, StandardCharsets.UTF_8); - content = this.restTemplate.getForObject(containsUrl, String.class); + content = this.restTemplate.getForObject(new java.net.URI(containsUrl), String.class); containsBundle = parser.parseResource(Bundle.class, content); assertNotNull(containsBundle.getEntry()); diff --git a/src/test/java/gov/nih/nci/evs/api/fhir/FhirVersioningStrategyTests.java b/src/test/java/gov/nih/nci/evs/api/fhir/FhirVersioningStrategyTests.java new file mode 100644 index 000000000..56d92af41 --- /dev/null +++ b/src/test/java/gov/nih/nci/evs/api/fhir/FhirVersioningStrategyTests.java @@ -0,0 +1,252 @@ +package gov.nih.nci.evs.api.fhir; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import gov.nih.nci.evs.api.properties.TestProperties; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** FHIR version strategy tests. */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +public class FhirVersioningStrategyTests { + + /** The Constant log. */ + private static final Logger log = LoggerFactory.getLogger(FhirVersioningStrategyTests.class); + + /** The port. */ + @LocalServerPort private int port; + + /** The rest template. */ + @Autowired private TestRestTemplate restTemplate; + + /** The test properties. */ + @SuppressWarnings("unused") + @Autowired + private TestProperties testProperties; + + /** The local host. */ + private final String localHost = "http://localhost:"; + + /** The parser R 4. */ + private static IParser parserR4; + + /** The parser R 5. */ + private static IParser parserR5; + + /** Sets the up once. */ + @BeforeAll + public static void setUpOnce() { + parserR4 = FhirContext.forR4().newJsonParser(); + parserR5 = FhirContext.forR5().newJsonParser(); + } + + /** + * Test code system lookup no version R 4. + * + * @throws Exception the exception + */ + @Test + public void testCodeSystemLookupNoVersionR4() throws Exception { + String url = null; + String code = null; + String endpoint = null; + String content = null; + org.hl7.fhir.r4.model.Parameters params = null; + + // NCI should be "latest monthly" not just "latest" + url = "http://ncicb.nci.nih.gov/xml/owl/EVS/Thesaurus.owl"; + code = "C3224"; + endpoint = localHost + port + "/fhir/r4/CodeSystem/$lookup?system=" + url + "&code=" + code; + content = this.restTemplate.getForObject(endpoint, String.class); + // log.info(" content = " + content); + params = parserR4.parseResource(org.hl7.fhir.r4.model.Parameters.class, content); + assertNotNull(params.getParameter("version")); + assertEquals("25.12e", params.getParameter("version").getValue().toString()); + + // MDR should be "latest" because monthly doesn't matter + url = "https://www.meddra.org"; + code = "10008906"; + endpoint = localHost + port + "/fhir/r4/CodeSystem/$lookup?system=" + url + "&code=" + code; + content = this.restTemplate.getForObject(endpoint, String.class); + // log.info(" content = " + content); + params = parserR4.parseResource(org.hl7.fhir.r4.model.Parameters.class, content); + assertNotNull(params.getParameter("version")); + assertEquals("28_0", params.getParameter("version").getValue().toString()); + } + + /** + * Test value set expand no version R 4. + * + * @throws Exception the exception + */ + @Test + public void testValueSetExpandNoVersionR4() throws Exception { + + String url = null; + String endpoint = null; + String content = null; + org.hl7.fhir.r4.model.ValueSet vs = null; + + // We expect "latest monthly" for "ncit" + url = "http://ncicb.nci.nih.gov/xml/owl/EVS/Thesaurus.owl?fhir_vs"; + endpoint = localHost + port + "/fhir/r4/ValueSet/$expand?url=" + url; + content = this.restTemplate.getForObject(endpoint, String.class); + + // log.info(" content = " + content); + vs = parserR4.parseResource(org.hl7.fhir.r4.model.ValueSet.class, content); + assertNotNull(vs.getVersion()); + assertEquals("25.12e", vs.getVersion()); + + // We expect "latest" for "mdr" + url = "https://www.meddra.org?fhir_vs"; + endpoint = localHost + port + "/fhir/r4/ValueSet/$expand?url=" + url; + content = this.restTemplate.getForObject(endpoint, String.class); + + // log.info(" content = " + content); + vs = parserR4.parseResource(org.hl7.fhir.r4.model.ValueSet.class, content); + assertNotNull(vs.getVersion()); + assertEquals("28_0", vs.getVersion()); + } + + /** + * Test concept map translate no version R 4. + * + * @throws Exception the exception + */ + @Test + public void testConceptMapTranslateNoVersionR4() throws Exception { + String url = "http://purl.obolibrary.org/obo/go.owl?fhir_cm=GO_to_NCIt_Mapping"; + String system = "http://purl.obolibrary.org/obo/go.owl"; + String code = "GO:0016887"; + String endpoint = + localHost + + port + + "/fhir/r4/ConceptMap/$translate?url=" + + url + + "&system=" + + system + + "&code=" + + code; + String content = this.restTemplate.getForObject(endpoint, String.class); + log.info(" content = " + content); + org.hl7.fhir.r4.model.Parameters params = + parserR4.parseResource(org.hl7.fhir.r4.model.Parameters.class, content); + assertNotNull(params.getParameter("result")); + assertTrue( + ((org.hl7.fhir.r4.model.BooleanType) params.getParameter("result").getValue()) + .booleanValue()); + } + + /** + * Test code system lookup no version R 5. + * + * @throws Exception the exception + */ + @Test + public void testCodeSystemLookupNoVersionR5() throws Exception { + String url = null; + String code = null; + String endpoint = null; + String content = null; + org.hl7.fhir.r5.model.Parameters params = null; + + // NCI should be "latest monthly" not just "latest" + url = "http://ncicb.nci.nih.gov/xml/owl/EVS/Thesaurus.owl"; + code = "C3224"; + endpoint = localHost + port + "/fhir/r5/CodeSystem/$lookup?system=" + url + "&code=" + code; + content = this.restTemplate.getForObject(endpoint, String.class); + log.info(" content = " + content); + params = parserR5.parseResource(org.hl7.fhir.r5.model.Parameters.class, content); + assertNotNull(params.getParameter("version")); + assertEquals("25.12e", params.getParameter("version").getValue().toString()); + + // MDR should be "latest" because monthly doesn't matter + url = "https://www.meddra.org"; + code = "10008906"; + endpoint = localHost + port + "/fhir/r5/CodeSystem/$lookup?system=" + url + "&code=" + code; + content = this.restTemplate.getForObject(endpoint, String.class); + log.info(" content = " + content); + params = parserR5.parseResource(org.hl7.fhir.r5.model.Parameters.class, content); + assertNotNull(params.getParameter("version")); + assertEquals("28_0", params.getParameter("version").getValue().toString()); + } + + /** + * Test value set expand no version R 5. + * + * @throws Exception the exception + */ + @Test + public void testValueSetExpandNoVersionR5() throws Exception { + + String url = null; + String endpoint = null; + String content = null; + org.hl7.fhir.r5.model.ValueSet vs = null; + + // We expect "latest monthly" for "ncit" + url = "http://ncicb.nci.nih.gov/xml/owl/EVS/Thesaurus.owl?fhir_vs"; + endpoint = localHost + port + "/fhir/r5/ValueSet/$expand?url=" + url; + content = this.restTemplate.getForObject(endpoint, String.class); + + // log.info(" content = " + content); + vs = parserR5.parseResource(org.hl7.fhir.r5.model.ValueSet.class, content); + assertNotNull(vs.getVersion()); + assertEquals("25.12e", vs.getVersion()); + + // We expect "latest" for "mdr" + url = "https://www.meddra.org?fhir_vs"; + endpoint = localHost + port + "/fhir/r5/ValueSet/$expand?url=" + url; + content = this.restTemplate.getForObject(endpoint, String.class); + + // log.info(" content = " + content); + vs = parserR5.parseResource(org.hl7.fhir.r5.model.ValueSet.class, content); + assertNotNull(vs.getVersion()); + assertEquals("28_0", vs.getVersion()); + } + + /** + * Test concept map translate no version R 5. + * + * @throws Exception the exception + */ + @Test + public void testConceptMapTranslateNoVersionR5() throws Exception { + String url = "http://purl.obolibrary.org/obo/go.owl?fhir_cm=GO_to_NCIt_Mapping"; + String system = "http://purl.obolibrary.org/obo/go.owl"; + String code = "GO:0016887"; + String endpoint = + localHost + + port + + "/fhir/r5/ConceptMap/$translate?url=" + + url + + "&system=" + + system + + "&sourceCode=" + + code; + String content = this.restTemplate.getForObject(endpoint, String.class); + log.info(" content = " + content); + org.hl7.fhir.r5.model.Parameters params = + parserR5.parseResource(org.hl7.fhir.r5.model.Parameters.class, content); + assertNotNull(params.getParameter("result")); + assertTrue( + ((org.hl7.fhir.r5.model.BooleanType) params.getParameter("result").getValue()) + .booleanValue()); + } +}