
001package ca.uhn.fhir.jpa.dao.search; 002 003/*- 004 * #%L 005 * HAPI FHIR JPA Server 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.context.RuntimeSearchParam; 025import ca.uhn.fhir.jpa.api.config.DaoConfig; 026import ca.uhn.fhir.jpa.model.entity.ModelConfig; 027import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamDate; 028import ca.uhn.fhir.jpa.model.entity.ResourceLink; 029import ca.uhn.fhir.jpa.model.search.ExtendedLuceneIndexData; 030import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor; 031import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams; 032import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 033import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 034import ca.uhn.fhir.util.MetaUtil; 035import com.google.common.base.Strings; 036import org.hl7.fhir.instance.model.api.IBase; 037import org.hl7.fhir.instance.model.api.IBaseCoding; 038import org.hl7.fhir.instance.model.api.IBaseResource; 039import org.jetbrains.annotations.NotNull; 040 041import java.util.ArrayList; 042import java.util.Collections; 043import java.util.HashMap; 044import java.util.List; 045import java.util.Locale; 046import java.util.Map; 047 048import static org.apache.commons.lang3.StringUtils.isNotBlank; 049 050/** 051 * Extract search params for advanced lucene indexing. 052 * <p> 053 * This class re-uses the extracted JPA entities to build an ExtendedLuceneIndexData instance. 054 */ 055public class ExtendedLuceneIndexExtractor { 056 057 private final DaoConfig myDaoConfig; 058 private final FhirContext myContext; 059 private final ResourceSearchParams myParams; 060 private final ISearchParamExtractor mySearchParamExtractor; 061 private final ModelConfig myModelConfig; 062 063 public ExtendedLuceneIndexExtractor(DaoConfig theDaoConfig, FhirContext theContext, ResourceSearchParams theActiveParams, 064 ISearchParamExtractor theSearchParamExtractor, ModelConfig theModelConfig) { 065 myDaoConfig = theDaoConfig; 066 myContext = theContext; 067 myParams = theActiveParams; 068 mySearchParamExtractor = theSearchParamExtractor; 069 myModelConfig = theModelConfig; 070 } 071 072 @NotNull 073 public ExtendedLuceneIndexData extract(IBaseResource theResource, ResourceIndexedSearchParams theNewParams) { 074 ExtendedLuceneIndexData retVal = new ExtendedLuceneIndexData(myContext, myModelConfig); 075 076 if(myDaoConfig.isStoreResourceInLuceneIndex()) { 077 retVal.setRawResourceData(myContext.newJsonParser().encodeResourceToString(theResource)); 078 } 079 080 retVal.setForcedId(theResource.getIdElement().getIdPart()); 081 082 extractAutocompleteTokens(theResource, retVal); 083 084 theNewParams.myStringParams.forEach(nextParam -> 085 retVal.addStringIndexData(nextParam.getParamName(), nextParam.getValueExact())); 086 087 theNewParams.myTokenParams.forEach(nextParam -> 088 retVal.addTokenIndexDataIfNotPresent(nextParam.getParamName(), nextParam.getSystem(), nextParam.getValue())); 089 090 theNewParams.myNumberParams.forEach(nextParam -> 091 retVal.addNumberIndexDataIfNotPresent(nextParam.getParamName(), nextParam.getValue())); 092 093 theNewParams.myDateParams.forEach(nextParam -> 094 retVal.addDateIndexData(nextParam.getParamName(), nextParam.getValueLow(), nextParam.getValueLowDateOrdinal(), 095 nextParam.getValueHigh(), nextParam.getValueHighDateOrdinal())); 096 097 theNewParams.myQuantityParams.forEach(nextParam -> 098 retVal.addQuantityIndexData(nextParam.getParamName(), nextParam.getUnits(), nextParam.getSystem(), nextParam.getValue().doubleValue())); 099 100 theResource.getMeta().getTag().forEach(tag -> 101 retVal.addTokenIndexData("_tag", tag)); 102 103 theResource.getMeta().getSecurity().forEach(sec -> 104 retVal.addTokenIndexData("_security", sec)); 105 106 theResource.getMeta().getProfile().forEach(prof -> 107 retVal.addUriIndexData("_profile", prof.getValue())); 108 109 String source = MetaUtil.getSource(myContext, theResource.getMeta()); 110 if (isNotBlank(source)) { 111 retVal.addUriIndexData("_source", source); 112 } 113 114 if (theResource.getMeta().getLastUpdated() != null) { 115 int ordinal = ResourceIndexedSearchParamDate.calculateOrdinalValue(theResource.getMeta().getLastUpdated()).intValue(); 116 retVal.addDateIndexData("_lastUpdated", theResource.getMeta().getLastUpdated(), ordinal, 117 theResource.getMeta().getLastUpdated(), ordinal); 118 } 119 120 121 if (!theNewParams.myLinks.isEmpty()) { 122 123 // awkwardly, links are indexed by jsonpath, not by search param. 124 // so we re-build the linkage. 125 Map<String, List<String>> linkPathToParamName = new HashMap<>(); 126 for (String nextParamName : theNewParams.getPopulatedResourceLinkParameters()) { 127 RuntimeSearchParam sp = myParams.get(nextParamName); 128 List<String> pathsSplit = sp.getPathsSplit(); 129 for (String nextPath : pathsSplit) { 130 // we want case-insensitive matching 131 nextPath = nextPath.toLowerCase(Locale.ROOT); 132 133 linkPathToParamName 134 .computeIfAbsent(nextPath, (p) -> new ArrayList<>()) 135 .add(nextParamName); 136 } 137 } 138 139 for (ResourceLink nextLink : theNewParams.getResourceLinks()) { 140 String insensitivePath = nextLink.getSourcePath().toLowerCase(Locale.ROOT); 141 List<String> paramNames = linkPathToParamName.getOrDefault(insensitivePath, Collections.emptyList()); 142 for (String nextParamName : paramNames) { 143 String qualifiedTargetResourceId = ""; 144 // Consider 2 cases for references 145 // Case 1: Resource Type and Resource ID is known 146 // Case 2: Resource is unknown and referred by canonical url reference 147 if(!Strings.isNullOrEmpty(nextLink.getTargetResourceId())) { 148 qualifiedTargetResourceId = nextLink.getTargetResourceType() + "/" + nextLink.getTargetResourceId(); 149 } else if(!Strings.isNullOrEmpty(nextLink.getTargetResourceUrl())) { 150 qualifiedTargetResourceId = nextLink.getTargetResourceUrl(); 151 } 152 retVal.addResourceLinkIndexData(nextParamName, qualifiedTargetResourceId); 153 } 154 } 155 } 156 157 return retVal; 158 } 159 160 /** 161 * Re-extract token parameters so we can distinguish 162 */ 163 private void extractAutocompleteTokens(IBaseResource theResource, ExtendedLuceneIndexData theRetVal) { 164 // we need to re-index token params to match up display with codes. 165 myParams.values().stream() 166 .filter(p->p.getParamType() == RestSearchParameterTypeEnum.TOKEN) 167 // TODO it would be nice to reuse TokenExtractor 168 .forEach(p-> mySearchParamExtractor.extractValues(p.getPath(), theResource) 169 .forEach(nextValue->indexTokenValue(theRetVal, p, nextValue) 170 )); 171 } 172 173 private void indexTokenValue(ExtendedLuceneIndexData theRetVal, RuntimeSearchParam p, IBase nextValue) { 174 String nextType = mySearchParamExtractor.toRootTypeName(nextValue); 175 String spName = p.getName(); 176 switch (nextType) { 177 case "CodeableConcept": 178 addToken_CodeableConcept(theRetVal, spName, nextValue); 179 break; 180 case "Coding": 181 addToken_Coding(theRetVal, spName, (IBaseCoding) nextValue); 182 break; 183 // TODO share this with TokenExtractor and introduce a ITokenIndexer interface. 184 // Ignore unknown types for now. 185 // This is just for autocomplete, and we are focused on Observation.code, category, combo-code, etc. 186// case "Identifier": 187// mySearchParamExtractor.addToken_Identifier(myResourceTypeName, params, searchParam, value); 188// break; 189// case "ContactPoint": 190// mySearchParamExtractor.addToken_ContactPoint(myResourceTypeName, params, searchParam, value); 191// break; 192 default: 193 break; 194 } 195 } 196 197 private void addToken_CodeableConcept(ExtendedLuceneIndexData theRetVal, String theSpName, IBase theValue) { 198 List<IBase> codings = mySearchParamExtractor.getCodingsFromCodeableConcept(theValue); 199 for (IBase nextCoding : codings) { 200 addToken_Coding(theRetVal, theSpName, (IBaseCoding) nextCoding); 201 } 202 } 203 204 private void addToken_Coding(ExtendedLuceneIndexData theRetVal, String theSpName, IBaseCoding theNextValue) { 205 theRetVal.addTokenIndexData(theSpName, theNextValue); 206 } 207}