
001/*- 002 * #%L 003 * HAPI FHIR JPA Server 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.dao.search; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeSearchParam; 024import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; 025import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; 026import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamQuantity; 027import ca.uhn.fhir.jpa.model.entity.ResourceLink; 028import ca.uhn.fhir.jpa.model.entity.ResourceTable; 029import ca.uhn.fhir.jpa.model.entity.TagDefinition; 030import ca.uhn.fhir.jpa.model.search.CompositeSearchIndexData; 031import ca.uhn.fhir.jpa.model.search.DateSearchIndexData; 032import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData; 033import ca.uhn.fhir.jpa.model.search.QuantitySearchIndexData; 034import ca.uhn.fhir.jpa.model.util.ResourceLinkUtils; 035import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; 036import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParamComposite; 037import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; 038import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 039import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 040import ca.uhn.fhir.util.MetaUtil; 041import com.google.common.base.Strings; 042import jakarta.annotation.Nonnull; 043import org.apache.commons.lang3.ObjectUtils; 044import org.hl7.fhir.instance.model.api.IBase; 045import org.hl7.fhir.instance.model.api.IBaseCoding; 046import org.hl7.fhir.instance.model.api.IBaseResource; 047 048import java.util.ArrayList; 049import java.util.Collections; 050import java.util.HashMap; 051import java.util.List; 052import java.util.Locale; 053import java.util.Map; 054 055import static org.apache.commons.lang3.StringUtils.isNotBlank; 056 057/** 058 * Extract search params for advanced HSearch indexing. 059 * <p> 060 * This class re-uses the extracted JPA entities to build an ExtendedHSearchIndexData instance. 061 */ 062public class ExtendedHSearchIndexExtractor { 063 064 private final JpaStorageSettings myJpaStorageSettings; 065 private final FhirContext myContext; 066 private final ResourceSearchParams myParams; 067 private final ISearchParamExtractor mySearchParamExtractor; 068 069 public ExtendedHSearchIndexExtractor( 070 JpaStorageSettings theJpaStorageSettings, 071 FhirContext theContext, 072 ResourceSearchParams theActiveParams, 073 ISearchParamExtractor theSearchParamExtractor) { 074 myJpaStorageSettings = theJpaStorageSettings; 075 myContext = theContext; 076 myParams = theActiveParams; 077 mySearchParamExtractor = theSearchParamExtractor; 078 } 079 080 @Nonnull 081 public ExtendedHSearchIndexData extract( 082 IBaseResource theResource, ResourceTable theEntity, ResourceIndexedSearchParams theNewParams) { 083 ExtendedHSearchIndexData retVal = 084 new ExtendedHSearchIndexData(myContext, myJpaStorageSettings, theResource, theEntity); 085 086 if (myJpaStorageSettings.isStoreResourceInHSearchIndex()) { 087 retVal.setRawResourceData(myContext.newJsonParser().encodeResourceToString(theResource)); 088 } 089 090 retVal.setForcedId(theResource.getIdElement().getIdPart()); 091 092 // todo add a flag ot StorageSettings to suppress this 093 extractAutocompleteTokens(theResource, retVal); 094 095 theNewParams.myStringParams.stream() 096 .filter(nextParam -> !nextParam.isMissing()) 097 .forEach(nextParam -> retVal.addStringIndexData(nextParam.getParamName(), nextParam.getValueExact())); 098 099 theNewParams.myTokenParams.stream() 100 .filter(nextParam -> !nextParam.isMissing()) 101 .forEach(nextParam -> retVal.addTokenIndexDataIfNotPresent( 102 nextParam.getParamName(), nextParam.getSystem(), nextParam.getValue())); 103 104 theNewParams.myNumberParams.stream() 105 .filter(nextParam -> !nextParam.isMissing()) 106 .forEach(nextParam -> 107 retVal.addNumberIndexDataIfNotPresent(nextParam.getParamName(), nextParam.getValue())); 108 109 theNewParams.myDateParams.stream() 110 .filter(nextParam -> !nextParam.isMissing()) 111 .forEach(nextParam -> retVal.addDateIndexData(nextParam.getParamName(), convertDate(nextParam))); 112 113 theNewParams.myQuantityParams.stream() 114 .filter(nextParam -> !nextParam.isMissing()) 115 .forEach( 116 nextParam -> retVal.addQuantityIndexData(nextParam.getParamName(), convertQuantity(nextParam))); 117 118 theNewParams.myUriParams.stream() 119 .filter(nextParam -> !nextParam.isMissing()) 120 .forEach(nextParam -> retVal.addUriIndexData(nextParam.getParamName(), nextParam.getUri())); 121 122 theEntity.getTags().forEach(tag -> { 123 TagDefinition td = tag.getTag(); 124 125 IBaseCoding coding = (IBaseCoding) myContext.getVersion().newCodingDt(); 126 coding.setVersion(td.getVersion()); 127 coding.setDisplay(td.getDisplay()); 128 coding.setCode(td.getCode()); 129 coding.setSystem(td.getSystem()); 130 coding.setUserSelected(ObjectUtils.defaultIfNull(td.getUserSelected(), false)); 131 switch (td.getTagType()) { 132 case TAG: 133 retVal.addTokenIndexData("_tag", coding); 134 break; 135 case PROFILE: 136 retVal.addUriIndexData("_profile", coding.getCode()); 137 break; 138 case SECURITY_LABEL: 139 retVal.addTokenIndexData("_security", coding); 140 break; 141 } 142 }); 143 144 String source = MetaUtil.getSource(myContext, theResource.getMeta()); 145 if (isNotBlank(source)) { 146 retVal.addUriIndexData("_source", source); 147 } 148 149 theNewParams.myCompositeParams.forEach(nextParam -> 150 retVal.addCompositeIndexData(nextParam.getSearchParamName(), buildCompositeIndexData(nextParam))); 151 152 if (theEntity.getUpdated() != null && !theEntity.getUpdated().isEmpty()) { 153 int ordinal = ResourceIndexedSearchParamDate.calculateOrdinalValue(theEntity.getUpdatedDate()) 154 .intValue(); 155 retVal.addDateIndexData( 156 "_lastUpdated", theEntity.getUpdatedDate(), ordinal, theEntity.getUpdatedDate(), ordinal); 157 } 158 159 if (!theNewParams.myLinks.isEmpty()) { 160 // awkwardly, links are indexed by jsonpath, not by search param. 161 // so we re-build the linkage. 162 Map<String, List<String>> linkPathToParamName = new HashMap<>(); 163 for (String nextParamName : theNewParams.getPopulatedResourceLinkParameters()) { 164 RuntimeSearchParam sp = myParams.get(nextParamName); 165 List<String> pathsSplit = sp.getPathsSplit(); 166 for (String nextPath : pathsSplit) { 167 // we want case-insensitive matching 168 nextPath = nextPath.toLowerCase(Locale.ROOT); 169 170 linkPathToParamName 171 .computeIfAbsent(nextPath, (p) -> new ArrayList<>()) 172 .add(nextParamName); 173 } 174 } 175 176 for (ResourceLink nextLink : theNewParams.getResourceLinks()) { 177 String insensitivePath = nextLink.getSourcePath().toLowerCase(Locale.ROOT); 178 List<String> paramNames = linkPathToParamName.getOrDefault(insensitivePath, Collections.emptyList()); 179 for (String nextParamName : paramNames) { 180 String qualifiedTargetResourceId = ""; 181 // Consider 2 cases for references 182 // Case 1: Resource Type and Resource ID is known 183 // Case 2: Resource is unknown and referred by canonical url reference 184 if (!Strings.isNullOrEmpty(nextLink.getTargetResourceId())) { 185 // non-canonical 186 assert !ResourceLinkUtils.UNKNOWN.equals(nextLink.getTargetResourceType()); 187 qualifiedTargetResourceId = 188 nextLink.getTargetResourceType() + "/" + nextLink.getTargetResourceId(); 189 } else if (!Strings.isNullOrEmpty(nextLink.getTargetResourceUrl())) { 190 // canonical 191 qualifiedTargetResourceId = nextLink.getTargetResourceUrl(); 192 } 193 retVal.addResourceLinkIndexData(nextParamName, qualifiedTargetResourceId); 194 } 195 } 196 } 197 198 return retVal; 199 } 200 201 @Nonnull 202 private CompositeSearchIndexData buildCompositeIndexData( 203 ResourceIndexedSearchParamComposite theSearchParamComposite) { 204 return new HSearchCompositeSearchIndexDataImpl(theSearchParamComposite); 205 } 206 207 /** 208 * Re-extract token parameters so we can distinguish 209 */ 210 private void extractAutocompleteTokens(IBaseResource theResource, ExtendedHSearchIndexData theRetVal) { 211 // we need to re-index token params to match up display with codes. 212 myParams.values().stream() 213 .filter(p -> p.getParamType() == RestSearchParameterTypeEnum.TOKEN) 214 // TODO it would be nice to reuse TokenExtractor 215 .forEach(p -> mySearchParamExtractor 216 .extractValues(p.getPath(), theResource) 217 .forEach(nextValue -> indexTokenValue(theRetVal, p, nextValue))); 218 } 219 220 private void indexTokenValue(ExtendedHSearchIndexData theRetVal, RuntimeSearchParam p, IBase nextValue) { 221 String nextType = mySearchParamExtractor.toRootTypeName(nextValue); 222 String spName = p.getName(); 223 switch (nextType) { 224 case "CodeableConcept": 225 addToken_CodeableConcept(theRetVal, spName, nextValue); 226 break; 227 case "Coding": 228 addToken_Coding(theRetVal, spName, (IBaseCoding) nextValue); 229 break; 230 // TODO share this with TokenExtractor and introduce a ITokenIndexer interface. 231 // Ignore unknown types for now. 232 // This is just for autocomplete, and we are focused on Observation.code, category, combo-code, etc. 233 // case "Identifier": 234 // mySearchParamExtractor.addToken_Identifier(myResourceTypeName, params, searchParam, value); 235 // break; 236 // case "ContactPoint": 237 // mySearchParamExtractor.addToken_ContactPoint(myResourceTypeName, params, searchParam, value); 238 // break; 239 default: 240 break; 241 } 242 } 243 244 private void addToken_CodeableConcept(ExtendedHSearchIndexData theRetVal, String theSpName, IBase theValue) { 245 List<IBase> codings = mySearchParamExtractor.getCodingsFromCodeableConcept(theValue); 246 for (IBase nextCoding : codings) { 247 addToken_Coding(theRetVal, theSpName, (IBaseCoding) nextCoding); 248 } 249 } 250 251 private void addToken_Coding(ExtendedHSearchIndexData theRetVal, String theSpName, IBaseCoding theNextValue) { 252 theRetVal.addTokenIndexData(theSpName, theNextValue); 253 } 254 255 @Nonnull 256 public static DateSearchIndexData convertDate(ResourceIndexedSearchParamDate nextParam) { 257 return new DateSearchIndexData( 258 nextParam.getValueLow(), 259 nextParam.getValueLowDateOrdinal(), 260 nextParam.getValueHigh(), 261 nextParam.getValueHighDateOrdinal()); 262 } 263 264 @Nonnull 265 public static QuantitySearchIndexData convertQuantity(ResourceIndexedSearchParamQuantity nextParam) { 266 return new QuantitySearchIndexData( 267 nextParam.getUnits(), 268 nextParam.getSystem(), 269 nextParam.getValue().doubleValue()); 270 } 271}