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