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