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}