
001/*- 002 * #%L 003 * HAPI FHIR JPA - Search Parameters 004 * %% 005 * Copyright (C) 2014 - 2025 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.RuntimeResourceDefinition; 024import ca.uhn.fhir.context.RuntimeSearchParam; 025import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 026import jakarta.annotation.Nonnull; 027import jakarta.annotation.Nullable; 028import org.apache.commons.lang3.Validate; 029import org.hl7.fhir.instance.model.api.IBaseResource; 030 031import java.util.Collection; 032import java.util.Collections; 033import java.util.HashMap; 034import java.util.List; 035import java.util.Map; 036import java.util.Set; 037import java.util.stream.Stream; 038 039import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; 040 041public class ReadOnlySearchParamCache { 042 043 // resourceName -> searchParamName -> searchparam 044 protected final Map<String, ResourceSearchParams> myResourceNameToSpNameToSp; 045 protected final Map<String, RuntimeSearchParam> myUrlToParam; 046 047 /** 048 * Constructor 049 */ 050 ReadOnlySearchParamCache() { 051 myResourceNameToSpNameToSp = new HashMap<>(); 052 myUrlToParam = new HashMap<>(); 053 } 054 055 /** 056 * Copy constructor 057 */ 058 private ReadOnlySearchParamCache(RuntimeSearchParamCache theRuntimeSearchParamCache) { 059 myResourceNameToSpNameToSp = theRuntimeSearchParamCache.myResourceNameToSpNameToSp; 060 myUrlToParam = theRuntimeSearchParamCache.myUrlToParam; 061 } 062 063 public Stream<RuntimeSearchParam> getSearchParamStream() { 064 return myResourceNameToSpNameToSp.values().stream().flatMap(entry -> entry.values().stream()); 065 } 066 067 protected ResourceSearchParams getSearchParamMap(String theResourceName) { 068 ResourceSearchParams retval = myResourceNameToSpNameToSp.get(theResourceName); 069 if (retval == null) { 070 return ResourceSearchParams.empty(theResourceName); 071 } 072 return retval.readOnly(); 073 } 074 075 public int size() { 076 return myResourceNameToSpNameToSp.size(); 077 } 078 079 @Nullable 080 public RuntimeSearchParam getByUrl(String theUrl) { 081 return myUrlToParam.get(theUrl); 082 } 083 084 public static ReadOnlySearchParamCache fromFhirContext( 085 @Nonnull FhirContext theFhirContext, @Nonnull SearchParameterCanonicalizer theCanonicalizer) { 086 return fromFhirContext(theFhirContext, theCanonicalizer, null); 087 } 088 089 public static ReadOnlySearchParamCache fromFhirContext( 090 @Nonnull FhirContext theFhirContext, 091 @Nonnull SearchParameterCanonicalizer theCanonicalizer, 092 @Nullable Set<String> theSearchParamPatternsToInclude) { 093 assert theCanonicalizer != null; 094 095 ReadOnlySearchParamCache retVal = new ReadOnlySearchParamCache(); 096 097 Set<String> resourceNames = theFhirContext.getResourceTypes(); 098 099 // Pull the list of search parameters out of the cached FhirContext 100 // (which avoids us getting a chain containing other JPA validation support providers) 101 FhirContext cachedCtx = 102 FhirContext.forCached(theFhirContext.getVersion().getVersion()); 103 List<IBaseResource> searchParams = cachedCtx.getValidationSupport().fetchAllSearchParameters(); 104 105 searchParams = defaultIfNull(searchParams, Collections.emptyList()); 106 for (IBaseResource next : searchParams) { 107 RuntimeSearchParam nextCanonical = theCanonicalizer.canonicalizeSearchParameter(next); 108 109 if (nextCanonical != null) { 110 111 // Force status to ACTIVE - For whatever reason the R5 draft SPs ship with 112 // a status of DRAFT which means the server doesn't actually apply them. 113 // At least this was the case as of 2021-12-24 - JA 114 nextCanonical = new RuntimeSearchParam( 115 nextCanonical.getId(), 116 nextCanonical.getUri(), 117 nextCanonical.getName(), 118 nextCanonical.getDescription(), 119 nextCanonical.getPath(), 120 nextCanonical.getParamType(), 121 nextCanonical.getProvidesMembershipInCompartments(), 122 nextCanonical.getTargets(), 123 RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE, 124 nextCanonical.getComboSearchParamType(), 125 nextCanonical.getComponents(), 126 nextCanonical.getBase()); 127 128 Collection<String> base = nextCanonical.getBase(); 129 if (base.contains("Resource") || base.contains("DomainResource")) { 130 base = resourceNames; 131 } 132 133 // Add it to our return value if permitted by the pattern parameters 134 for (String nextResourceName : base) { 135 ResourceSearchParams resourceSearchParams = retVal.myResourceNameToSpNameToSp.computeIfAbsent( 136 nextResourceName, t -> new ResourceSearchParams(nextResourceName)); 137 String nextParamName = nextCanonical.getName(); 138 if (theSearchParamPatternsToInclude == null 139 || searchParamMatchesAtLeastOnePattern( 140 theSearchParamPatternsToInclude, nextResourceName, nextParamName)) { 141 resourceSearchParams.addSearchParamIfAbsent(nextParamName, nextCanonical); 142 } 143 } 144 } 145 } 146 147 // Now grab all the runtime search parameters from the resource definitions 148 for (String resourceName : resourceNames) { 149 RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName); 150 String nextResourceName = nextResDef.getName(); 151 152 ResourceSearchParams resourceSearchParams = retVal.myResourceNameToSpNameToSp.computeIfAbsent( 153 nextResourceName, t -> new ResourceSearchParams(nextResourceName)); 154 for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) { 155 String nextParamName = nextSp.getName(); 156 // Add it to our return value if permitted by the pattern parameters 157 if (theSearchParamPatternsToInclude == null 158 || searchParamMatchesAtLeastOnePattern( 159 theSearchParamPatternsToInclude, nextResourceName, nextParamName)) { 160 resourceSearchParams.addSearchParamIfAbsent(nextParamName, nextSp); 161 } 162 } 163 } 164 return retVal; 165 } 166 167 public static boolean searchParamMatchesAtLeastOnePattern( 168 Set<String> theSearchParamPatterns, String theResourceType, String theSearchParamName) { 169 for (String nextPattern : theSearchParamPatterns) { 170 if ("*".equals(nextPattern)) { 171 return true; 172 } 173 int colonIdx = nextPattern.indexOf(':'); 174 Validate.isTrue(colonIdx > 0, "Invalid search param pattern: %s", nextPattern); 175 String resourceType = nextPattern.substring(0, colonIdx); 176 String searchParamName = nextPattern.substring(colonIdx + 1); 177 Validate.notBlank(resourceType, "No resource type specified in pattern: %s", nextPattern); 178 Validate.notBlank(searchParamName, "No param name specified in pattern: %s", nextPattern); 179 if (!resourceType.equals("*") && !resourceType.equals(theResourceType)) { 180 continue; 181 } 182 if (!searchParamName.equals("*") && !searchParamName.equals(theSearchParamName)) { 183 continue; 184 } 185 return true; 186 } 187 188 return false; 189 } 190 191 public static ReadOnlySearchParamCache fromRuntimeSearchParamCache( 192 RuntimeSearchParamCache theRuntimeSearchParamCache) { 193 return new ReadOnlySearchParamCache(theRuntimeSearchParamCache); 194 } 195}