
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 public RuntimeSearchParam getByUrl(String theUrl) { 080 return myUrlToParam.get(theUrl); 081 } 082 083 public static ReadOnlySearchParamCache fromFhirContext( 084 @Nonnull FhirContext theFhirContext, @Nonnull SearchParameterCanonicalizer theCanonicalizer) { 085 return fromFhirContext(theFhirContext, theCanonicalizer, null); 086 } 087 088 public static ReadOnlySearchParamCache fromFhirContext( 089 @Nonnull FhirContext theFhirContext, 090 @Nonnull SearchParameterCanonicalizer theCanonicalizer, 091 @Nullable Set<String> theSearchParamPatternsToInclude) { 092 assert theCanonicalizer != null; 093 094 ReadOnlySearchParamCache retVal = new ReadOnlySearchParamCache(); 095 096 Set<String> resourceNames = theFhirContext.getResourceTypes(); 097 098 // Pull the list of search parameters out of the cached FhirContext 099 // (which avoids us getting a chain containing other JPA validation support providers) 100 FhirContext cachedCtx = 101 FhirContext.forCached(theFhirContext.getVersion().getVersion()); 102 List<IBaseResource> searchParams = cachedCtx.getValidationSupport().fetchAllSearchParameters(); 103 104 searchParams = defaultIfNull(searchParams, Collections.emptyList()); 105 for (IBaseResource next : searchParams) { 106 RuntimeSearchParam nextCanonical = theCanonicalizer.canonicalizeSearchParameter(next); 107 108 if (nextCanonical != null) { 109 110 // Force status to ACTIVE - For whatever reason the R5 draft SPs ship with 111 // a status of DRAFT which means the server doesn't actually apply them. 112 // At least this was the case as of 2021-12-24 - JA 113 nextCanonical = new RuntimeSearchParam( 114 nextCanonical.getId(), 115 nextCanonical.getUri(), 116 nextCanonical.getName(), 117 nextCanonical.getDescription(), 118 nextCanonical.getPath(), 119 nextCanonical.getParamType(), 120 nextCanonical.getProvidesMembershipInCompartments(), 121 nextCanonical.getTargets(), 122 RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE, 123 nextCanonical.getComboSearchParamType(), 124 nextCanonical.getComponents(), 125 nextCanonical.getBase()); 126 127 Collection<String> base = nextCanonical.getBase(); 128 if (base.contains("Resource") || base.contains("DomainResource")) { 129 base = resourceNames; 130 } 131 132 // Add it to our return value if permitted by the pattern parameters 133 for (String nextResourceName : base) { 134 ResourceSearchParams resourceSearchParams = retVal.myResourceNameToSpNameToSp.computeIfAbsent( 135 nextResourceName, t -> new ResourceSearchParams(nextResourceName)); 136 String nextParamName = nextCanonical.getName(); 137 if (theSearchParamPatternsToInclude == null 138 || searchParamMatchesAtLeastOnePattern( 139 theSearchParamPatternsToInclude, nextResourceName, nextParamName)) { 140 resourceSearchParams.addSearchParamIfAbsent(nextParamName, nextCanonical); 141 } 142 } 143 } 144 } 145 146 // Now grab all the runtime search parameters from the resource definitions 147 for (String resourceName : resourceNames) { 148 RuntimeResourceDefinition nextResDef = theFhirContext.getResourceDefinition(resourceName); 149 String nextResourceName = nextResDef.getName(); 150 151 ResourceSearchParams resourceSearchParams = retVal.myResourceNameToSpNameToSp.computeIfAbsent( 152 nextResourceName, t -> new ResourceSearchParams(nextResourceName)); 153 for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) { 154 String nextParamName = nextSp.getName(); 155 // Add it to our return value if permitted by the pattern parameters 156 if (theSearchParamPatternsToInclude == null 157 || searchParamMatchesAtLeastOnePattern( 158 theSearchParamPatternsToInclude, nextResourceName, nextParamName)) { 159 resourceSearchParams.addSearchParamIfAbsent(nextParamName, nextSp); 160 } 161 } 162 } 163 return retVal; 164 } 165 166 public static boolean searchParamMatchesAtLeastOnePattern( 167 Set<String> theSearchParamPatterns, String theResourceType, String theSearchParamName) { 168 for (String nextPattern : theSearchParamPatterns) { 169 if ("*".equals(nextPattern)) { 170 return true; 171 } 172 int colonIdx = nextPattern.indexOf(':'); 173 Validate.isTrue(colonIdx > 0, "Invalid search param pattern: %s", nextPattern); 174 String resourceType = nextPattern.substring(0, colonIdx); 175 String searchParamName = nextPattern.substring(colonIdx + 1); 176 Validate.notBlank(resourceType, "No resource type specified in pattern: %s", nextPattern); 177 Validate.notBlank(searchParamName, "No param name specified in pattern: %s", nextPattern); 178 if (!resourceType.equals("*") && !resourceType.equals(theResourceType)) { 179 continue; 180 } 181 if (!searchParamName.equals("*") && !searchParamName.equals(theSearchParamName)) { 182 continue; 183 } 184 return true; 185 } 186 187 return false; 188 } 189 190 public static ReadOnlySearchParamCache fromRuntimeSearchParamCache( 191 RuntimeSearchParamCache theRuntimeSearchParamCache) { 192 return new ReadOnlySearchParamCache(theRuntimeSearchParamCache); 193 } 194}