001/*- 002 * #%L 003 * HAPI FHIR JPA - Search Parameters 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.searchparam.registry; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.FhirVersionEnum; 024import ca.uhn.fhir.context.RuntimeResourceDefinition; 025import ca.uhn.fhir.context.RuntimeSearchParam; 026import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 027import ca.uhn.fhir.util.BundleUtil; 028import ca.uhn.fhir.util.ClasspathUtil; 029import jakarta.annotation.Nonnull; 030import jakarta.annotation.Nullable; 031import org.apache.commons.lang3.Validate; 032import org.hl7.fhir.instance.model.api.IBaseBundle; 033import org.hl7.fhir.instance.model.api.IBaseResource; 034 035import java.util.Collection; 036import java.util.Collections; 037import java.util.HashMap; 038import java.util.List; 039import java.util.Map; 040import java.util.Set; 041import java.util.stream.Stream; 042 043import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; 044 045public class ReadOnlySearchParamCache { 046 047 // resourceName -> searchParamName -> searchparam 048 protected final Map<String, ResourceSearchParams> myResourceNameToSpNameToSp; 049 protected final Map<String, RuntimeSearchParam> myUrlToParam; 050 051 /** 052 * Constructor 053 */ 054 ReadOnlySearchParamCache() { 055 myResourceNameToSpNameToSp = new HashMap<>(); 056 myUrlToParam = new HashMap<>(); 057 } 058 059 /** 060 * Copy constructor 061 */ 062 private ReadOnlySearchParamCache(RuntimeSearchParamCache theRuntimeSearchParamCache) { 063 myResourceNameToSpNameToSp = theRuntimeSearchParamCache.myResourceNameToSpNameToSp; 064 myUrlToParam = theRuntimeSearchParamCache.myUrlToParam; 065 } 066 067 public Stream<RuntimeSearchParam> getSearchParamStream() { 068 return myResourceNameToSpNameToSp.values().stream().flatMap(entry -> entry.values().stream()); 069 } 070 071 protected ResourceSearchParams getSearchParamMap(String theResourceName) { 072 ResourceSearchParams retval = myResourceNameToSpNameToSp.get(theResourceName); 073 if (retval == null) { 074 return ResourceSearchParams.empty(theResourceName); 075 } 076 return retval.readOnly(); 077 } 078 079 public int size() { 080 return myResourceNameToSpNameToSp.size(); 081 } 082 083 public RuntimeSearchParam getByUrl(String theUrl) { 084 return myUrlToParam.get(theUrl); 085 } 086 087 public static ReadOnlySearchParamCache fromFhirContext( 088 @Nonnull FhirContext theFhirContext, @Nonnull SearchParameterCanonicalizer theCanonicalizer) { 089 return fromFhirContext(theFhirContext, theCanonicalizer, null); 090 } 091 092 public static ReadOnlySearchParamCache fromFhirContext( 093 @Nonnull FhirContext theFhirContext, 094 @Nonnull SearchParameterCanonicalizer theCanonicalizer, 095 @Nullable Set<String> theSearchParamPatternsToInclude) { 096 assert theCanonicalizer != null; 097 098 ReadOnlySearchParamCache retVal = new ReadOnlySearchParamCache(); 099 100 Set<String> resourceNames = theFhirContext.getResourceTypes(); 101 102 /* 103 * For R4/R4B we include a Bundle of SearchParameters from the core spec in 104 * hapi-fhir-validation-resources-r4/r4b 105 * 106 * For R5 we include the NPM core R5 packages in 107 * hapi-fhir-validation-resources-r5, so we get them from it 108 */ 109 List<IBaseResource> searchParams = null; 110 if (theFhirContext.getVersion().getVersion() == FhirVersionEnum.R4) { 111 IBaseBundle allSearchParameterBundle = (IBaseBundle) theFhirContext 112 .newJsonParser() 113 .parseResource( 114 ClasspathUtil.loadResourceAsStream("org/hl7/fhir/r4/model/sp/search-parameters.json")); 115 searchParams = BundleUtil.toListOfResources(theFhirContext, allSearchParameterBundle); 116 } else if (theFhirContext.getVersion().getVersion() == FhirVersionEnum.R4B) { 117 IBaseBundle allSearchParameterBundle = (IBaseBundle) theFhirContext 118 .newXmlParser() 119 .parseResource( 120 ClasspathUtil.loadResourceAsStream("org/hl7/fhir/r4b/model/sp/search-parameters.xml")); 121 searchParams = BundleUtil.toListOfResources(theFhirContext, allSearchParameterBundle); 122 } else if (theFhirContext.getVersion().getVersion() == FhirVersionEnum.R5) { 123 searchParams = FhirContext.forR5Cached().getValidationSupport().fetchAllSearchParameters(); 124 } 125 126 searchParams = defaultIfNull(searchParams, Collections.emptyList()); 127 for (IBaseResource next : searchParams) { 128 RuntimeSearchParam nextCanonical = theCanonicalizer.canonicalizeSearchParameter(next); 129 130 if (nextCanonical != null) { 131 132 // Force status to ACTIVE - For whatever reason the R5 draft SPs ship with 133 // a status of DRAFT which means the server doesn't actually apply them. 134 // At least this was the case as of 2021-12-24 - JA 135 nextCanonical = new RuntimeSearchParam( 136 nextCanonical.getId(), 137 nextCanonical.getUri(), 138 nextCanonical.getName(), 139 nextCanonical.getDescription(), 140 nextCanonical.getPath(), 141 nextCanonical.getParamType(), 142 nextCanonical.getProvidesMembershipInCompartments(), 143 nextCanonical.getTargets(), 144 RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE, 145 nextCanonical.getComboSearchParamType(), 146 nextCanonical.getComponents(), 147 nextCanonical.getBase()); 148 149 Collection<String> base = nextCanonical.getBase(); 150 if (base.contains("Resource") || base.contains("DomainResource")) { 151 base = resourceNames; 152 } 153 154 // Add it to our return value if permitted by the pattern parameters 155 for (String nextResourceName : base) { 156 ResourceSearchParams resourceSearchParams = retVal.myResourceNameToSpNameToSp.computeIfAbsent( 157 nextResourceName, t -> new ResourceSearchParams(nextResourceName)); 158 String nextParamName = nextCanonical.getName(); 159 if (theSearchParamPatternsToInclude == null 160 || searchParamMatchesAtLeastOnePattern( 161 theSearchParamPatternsToInclude, nextResourceName, nextParamName)) { 162 resourceSearchParams.addSearchParamIfAbsent(nextParamName, nextCanonical); 163 } 164 } 165 } 166 } 167 168 // Now grab all the runtime search parameters from the resource definitions 169 for (String resourceName : resourceNames) { 170 RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName); 171 String nextResourceName = nextResDef.getName(); 172 173 ResourceSearchParams resourceSearchParams = retVal.myResourceNameToSpNameToSp.computeIfAbsent( 174 nextResourceName, t -> new ResourceSearchParams(nextResourceName)); 175 for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) { 176 String nextParamName = nextSp.getName(); 177 // Add it to our return value if permitted by the pattern parameters 178 if (theSearchParamPatternsToInclude == null 179 || searchParamMatchesAtLeastOnePattern( 180 theSearchParamPatternsToInclude, nextResourceName, nextParamName)) { 181 resourceSearchParams.addSearchParamIfAbsent(nextParamName, nextSp); 182 } 183 } 184 } 185 return retVal; 186 } 187 188 public static boolean searchParamMatchesAtLeastOnePattern( 189 Set<String> theSearchParamPatterns, String theResourceType, String theSearchParamName) { 190 for (String nextPattern : theSearchParamPatterns) { 191 if ("*".equals(nextPattern)) { 192 return true; 193 } 194 int colonIdx = nextPattern.indexOf(':'); 195 Validate.isTrue(colonIdx > 0, "Invalid search param pattern: %s", nextPattern); 196 String resourceType = nextPattern.substring(0, colonIdx); 197 String searchParamName = nextPattern.substring(colonIdx + 1); 198 Validate.notBlank(resourceType, "No resource type specified in pattern: %s", nextPattern); 199 Validate.notBlank(searchParamName, "No param name specified in pattern: %s", nextPattern); 200 if (!resourceType.equals("*") && !resourceType.equals(theResourceType)) { 201 continue; 202 } 203 if (!searchParamName.equals("*") && !searchParamName.equals(theSearchParamName)) { 204 continue; 205 } 206 return true; 207 } 208 209 return false; 210 } 211 212 public static ReadOnlySearchParamCache fromRuntimeSearchParamCache( 213 RuntimeSearchParamCache theRuntimeSearchParamCache) { 214 return new ReadOnlySearchParamCache(theRuntimeSearchParamCache); 215 } 216}