001/*-
002 * #%L
003 * HAPI FHIR Search Parameters
004 * %%
005 * Copyright (C) 2014 - 2023 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.searchparam.extractor;
021
022import ca.uhn.fhir.context.RuntimeSearchParam;
023import ca.uhn.fhir.jpa.model.config.PartitionSettings;
024import ca.uhn.fhir.jpa.model.entity.StorageSettings;
025import ca.uhn.fhir.jpa.model.entity.*;
026import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
027import ca.uhn.fhir.jpa.searchparam.util.RuntimeSearchParamHelper;
028import ca.uhn.fhir.model.api.IQueryParameterType;
029import ca.uhn.fhir.rest.api.Constants;
030import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
031import ca.uhn.fhir.rest.param.QuantityParam;
032import ca.uhn.fhir.rest.param.ReferenceParam;
033import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
034import org.apache.commons.lang3.StringUtils;
035
036import java.util.ArrayList;
037import java.util.Collection;
038import java.util.Collections;
039import java.util.Date;
040import java.util.HashSet;
041import java.util.List;
042import java.util.Set;
043import java.util.function.Predicate;
044import javax.annotation.Nonnull;
045
046import static org.apache.commons.lang3.StringUtils.compare;
047import static org.apache.commons.lang3.StringUtils.isNotBlank;
048
049public final class ResourceIndexedSearchParams {
050        public final Collection<ResourceIndexedSearchParamString> myStringParams = new ArrayList<>();
051        public final Collection<ResourceIndexedSearchParamToken> myTokenParams = new HashSet<>();
052        public final Collection<ResourceIndexedSearchParamNumber> myNumberParams = new ArrayList<>();
053        public final Collection<ResourceIndexedSearchParamQuantity> myQuantityParams = new ArrayList<>();
054        public final Collection<ResourceIndexedSearchParamQuantityNormalized> myQuantityNormalizedParams =
055                        new ArrayList<>();
056        public final Collection<ResourceIndexedSearchParamDate> myDateParams = new ArrayList<>();
057        public final Collection<ResourceIndexedSearchParamUri> myUriParams = new ArrayList<>();
058        public final Collection<ResourceIndexedSearchParamCoords> myCoordsParams = new ArrayList<>();
059
060        public final Collection<ResourceIndexedComboStringUnique> myComboStringUniques = new HashSet<>();
061        public final Collection<ResourceIndexedComboTokenNonUnique> myComboTokenNonUnique = new HashSet<>();
062        public final Collection<ResourceLink> myLinks = new HashSet<>();
063        public final Set<String> myPopulatedResourceLinkParameters = new HashSet<>();
064        public final Collection<SearchParamPresentEntity> mySearchParamPresentEntities = new HashSet<>();
065        public final Collection<ResourceIndexedSearchParamComposite> myCompositeParams = new HashSet<>();
066
067        private static final Set<String> myIgnoredParams = Set.of(Constants.PARAM_TEXT, Constants.PARAM_CONTENT);
068
069        public ResourceIndexedSearchParams() {}
070
071        public ResourceIndexedSearchParams(ResourceTable theEntity) {
072                if (theEntity.isParamsStringPopulated()) {
073                        myStringParams.addAll(theEntity.getParamsString());
074                }
075                if (theEntity.isParamsTokenPopulated()) {
076                        myTokenParams.addAll(theEntity.getParamsToken());
077                }
078                if (theEntity.isParamsNumberPopulated()) {
079                        myNumberParams.addAll(theEntity.getParamsNumber());
080                }
081                if (theEntity.isParamsQuantityPopulated()) {
082                        myQuantityParams.addAll(theEntity.getParamsQuantity());
083                }
084                if (theEntity.isParamsQuantityNormalizedPopulated()) {
085                        myQuantityNormalizedParams.addAll(theEntity.getParamsQuantityNormalized());
086                }
087                if (theEntity.isParamsDatePopulated()) {
088                        myDateParams.addAll(theEntity.getParamsDate());
089                }
090                if (theEntity.isParamsUriPopulated()) {
091                        myUriParams.addAll(theEntity.getParamsUri());
092                }
093                if (theEntity.isParamsCoordsPopulated()) {
094                        myCoordsParams.addAll(theEntity.getParamsCoords());
095                }
096                if (theEntity.isHasLinks()) {
097                        myLinks.addAll(theEntity.getResourceLinks());
098                }
099
100                if (theEntity.isParamsComboStringUniquePresent()) {
101                        myComboStringUniques.addAll(theEntity.getParamsComboStringUnique());
102                }
103                if (theEntity.isParamsComboTokensNonUniquePresent()) {
104                        myComboTokenNonUnique.addAll(theEntity.getmyParamsComboTokensNonUnique());
105                }
106        }
107
108        public Collection<ResourceLink> getResourceLinks() {
109                return myLinks;
110        }
111
112        public void populateResourceTableSearchParamsPresentFlags(ResourceTable theEntity) {
113                theEntity.setParamsStringPopulated(myStringParams.isEmpty() == false);
114                theEntity.setParamsTokenPopulated(myTokenParams.isEmpty() == false);
115                theEntity.setParamsNumberPopulated(myNumberParams.isEmpty() == false);
116                theEntity.setParamsQuantityPopulated(myQuantityParams.isEmpty() == false);
117                theEntity.setParamsQuantityNormalizedPopulated(myQuantityNormalizedParams.isEmpty() == false);
118                theEntity.setParamsDatePopulated(myDateParams.isEmpty() == false);
119                theEntity.setParamsUriPopulated(myUriParams.isEmpty() == false);
120                theEntity.setParamsCoordsPopulated(myCoordsParams.isEmpty() == false);
121                theEntity.setParamsComboStringUniquePresent(myComboStringUniques.isEmpty() == false);
122                theEntity.setParamsComboTokensNonUniquePresent(myComboTokenNonUnique.isEmpty() == false);
123                theEntity.setHasLinks(myLinks.isEmpty() == false);
124        }
125
126        public void populateResourceTableParamCollections(ResourceTable theEntity) {
127                theEntity.setParamsString(myStringParams);
128                theEntity.setParamsToken(myTokenParams);
129                theEntity.setParamsNumber(myNumberParams);
130                theEntity.setParamsQuantity(myQuantityParams);
131                theEntity.setParamsQuantityNormalized(myQuantityNormalizedParams);
132                theEntity.setParamsDate(myDateParams);
133                theEntity.setParamsUri(myUriParams);
134                theEntity.setParamsCoords(myCoordsParams);
135                theEntity.setResourceLinks(myLinks);
136        }
137
138        public void updateSpnamePrefixForIndexOnUpliftedChain(String theContainingType, String theSpnamePrefix) {
139                updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myNumberParams, theSpnamePrefix);
140                updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myQuantityParams, theSpnamePrefix);
141                updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myQuantityNormalizedParams, theSpnamePrefix);
142                updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myDateParams, theSpnamePrefix);
143                updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myUriParams, theSpnamePrefix);
144                updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myTokenParams, theSpnamePrefix);
145                updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myStringParams, theSpnamePrefix);
146                updateSpnamePrefixForIndexOnUpliftedChain(theContainingType, myCoordsParams, theSpnamePrefix);
147        }
148
149        public void updateSpnamePrefixForLinksOnContainedResource(String theSpNamePrefix) {
150                for (ResourceLink param : myLinks) {
151                        // The resource link already has the resource type of the contained resource at the head of the path.
152                        // We need to replace this with the name of the containing type, and extend the search path.
153                        int index = param.getSourcePath().indexOf('.');
154                        if (index > -1) {
155                                param.setSourcePath(theSpNamePrefix + param.getSourcePath().substring(index));
156                        } else {
157                                // Can this ever happen?
158                                param.setSourcePath(theSpNamePrefix + "." + param.getSourcePath());
159                        }
160                        param.calculateHashes(); // re-calculateHashes
161                }
162        }
163
164        void setUpdatedTime(Date theUpdateTime) {
165                setUpdatedTime(myStringParams, theUpdateTime);
166                setUpdatedTime(myNumberParams, theUpdateTime);
167                setUpdatedTime(myQuantityParams, theUpdateTime);
168                setUpdatedTime(myQuantityNormalizedParams, theUpdateTime);
169                setUpdatedTime(myDateParams, theUpdateTime);
170                setUpdatedTime(myUriParams, theUpdateTime);
171                setUpdatedTime(myCoordsParams, theUpdateTime);
172                setUpdatedTime(myTokenParams, theUpdateTime);
173        }
174
175        private void setUpdatedTime(Collection<? extends BaseResourceIndexedSearchParam> theParams, Date theUpdateTime) {
176                for (BaseResourceIndexedSearchParam nextSearchParam : theParams) {
177                        nextSearchParam.setUpdated(theUpdateTime);
178                }
179        }
180
181        private void updateSpnamePrefixForIndexOnUpliftedChain(
182                        String theContainingType,
183                        Collection<? extends BaseResourceIndexedSearchParam> theParams,
184                        @Nonnull String theSpnamePrefix) {
185
186                for (BaseResourceIndexedSearchParam param : theParams) {
187                        param.setResourceType(theContainingType);
188                        param.setParamName(theSpnamePrefix + "." + param.getParamName());
189
190                        // re-calculate hashes
191                        param.calculateHashes();
192                }
193        }
194
195        public Set<String> getPopulatedResourceLinkParameters() {
196                return myPopulatedResourceLinkParameters;
197        }
198
199        public boolean matchParam(
200                        StorageSettings theStorageSettings,
201                        String theResourceName,
202                        String theParamName,
203                        RuntimeSearchParam theParamDef,
204                        IQueryParameterType theValue) {
205
206                if (theParamDef == null) {
207                        return false;
208                }
209                Collection<? extends BaseResourceIndexedSearchParam> resourceParams = null;
210                IQueryParameterType value = theValue;
211                switch (theParamDef.getParamType()) {
212                        case TOKEN:
213                                resourceParams = myTokenParams;
214                                break;
215                        case QUANTITY:
216                                if (theStorageSettings
217                                                .getNormalizedQuantitySearchLevel()
218                                                .equals(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_SUPPORTED)) {
219                                        QuantityParam quantity = QuantityParam.toQuantityParam(theValue);
220                                        QuantityParam normalized = UcumServiceUtil.toCanonicalQuantityOrNull(quantity);
221                                        if (normalized != null) {
222                                                resourceParams = myQuantityNormalizedParams;
223                                                value = normalized;
224                                        }
225                                }
226
227                                if (resourceParams == null) {
228                                        resourceParams = myQuantityParams;
229                                }
230                                break;
231                        case STRING:
232                                resourceParams = myStringParams;
233                                break;
234                        case NUMBER:
235                                resourceParams = myNumberParams;
236                                break;
237                        case URI:
238                                resourceParams = myUriParams;
239                                break;
240                        case DATE:
241                                resourceParams = myDateParams;
242                                break;
243                        case REFERENCE:
244                                return matchResourceLinks(
245                                                theStorageSettings,
246                                                theResourceName,
247                                                theParamName,
248                                                value,
249                                                theParamDef.getPathsSplitForResourceType(theResourceName));
250                        case COMPOSITE:
251                        case HAS:
252                        case SPECIAL:
253                        default:
254                                resourceParams = null;
255                }
256                if (resourceParams == null) {
257                        return false;
258                }
259
260                for (BaseResourceIndexedSearchParam nextParam : resourceParams) {
261                        if (nextParam.getParamName().equalsIgnoreCase(theParamName)) {
262                                if (nextParam.matches(value)) {
263                                        return true;
264                                }
265                        }
266                }
267
268                return false;
269        }
270
271        /**
272         * @deprecated Replace with the method below
273         */
274        // KHS This needs to be public as libraries outside of hapi call it directly
275        @Deprecated
276        public boolean matchResourceLinks(
277                        String theResourceName, String theParamName, IQueryParameterType theParam, String theParamPath) {
278                return matchResourceLinks(new StorageSettings(), theResourceName, theParamName, theParam, theParamPath);
279        }
280
281        public boolean matchResourceLinks(
282                        StorageSettings theStorageSettings,
283                        String theResourceName,
284                        String theParamName,
285                        IQueryParameterType theParam,
286                        List<String> theParamPaths) {
287                for (String nextPath : theParamPaths) {
288                        if (matchResourceLinks(theStorageSettings, theResourceName, theParamName, theParam, nextPath)) {
289                                return true;
290                        }
291                }
292                return false;
293        }
294
295        // KHS This needs to be public as libraries outside of hapi call it directly
296        public boolean matchResourceLinks(
297                        StorageSettings theStorageSettings,
298                        String theResourceName,
299                        String theParamName,
300                        IQueryParameterType theParam,
301                        String theParamPath) {
302                ReferenceParam reference = (ReferenceParam) theParam;
303
304                Predicate<ResourceLink> namedParamPredicate =
305                                resourceLink -> searchParameterPathMatches(theResourceName, resourceLink, theParamName, theParamPath)
306                                                && resourceIdMatches(theStorageSettings, resourceLink, reference);
307
308                return myLinks.stream().anyMatch(namedParamPredicate);
309        }
310
311        private boolean resourceIdMatches(
312                        StorageSettings theStorageSettings, ResourceLink theResourceLink, ReferenceParam theReference) {
313                String baseUrl = theReference.getBaseUrl();
314                if (isNotBlank(baseUrl)) {
315                        if (!theStorageSettings.getTreatBaseUrlsAsLocal().contains(baseUrl)) {
316                                return false;
317                        }
318                }
319
320                String targetType = theResourceLink.getTargetResourceType();
321                String targetId = theResourceLink.getTargetResourceId();
322
323                assert isNotBlank(targetType);
324                assert isNotBlank(targetId);
325
326                if (theReference.hasResourceType()) {
327                        if (!theReference.getResourceType().equals(targetType)) {
328                                return false;
329                        }
330                }
331
332                if (!targetId.equals(theReference.getIdPart())) {
333                        return false;
334                }
335
336                return true;
337        }
338
339        private boolean searchParameterPathMatches(
340                        String theResourceName, ResourceLink theResourceLink, String theParamName, String theParamPath) {
341                String sourcePath = theResourceLink.getSourcePath();
342                return sourcePath.equalsIgnoreCase(theParamPath);
343        }
344
345        @Override
346        public String toString() {
347                return "ResourceIndexedSearchParams{" + "stringParams="
348                                + myStringParams + ", tokenParams="
349                                + myTokenParams + ", numberParams="
350                                + myNumberParams + ", quantityParams="
351                                + myQuantityParams + ", quantityNormalizedParams="
352                                + myQuantityNormalizedParams + ", dateParams="
353                                + myDateParams + ", uriParams="
354                                + myUriParams + ", coordsParams="
355                                + myCoordsParams + ", comboStringUniques="
356                                + myComboStringUniques + ", comboTokenNonUniques="
357                                + myComboTokenNonUnique + ", links="
358                                + myLinks + '}';
359        }
360
361        public void findMissingSearchParams(
362                        PartitionSettings thePartitionSettings,
363                        StorageSettings theStorageSettings,
364                        ResourceTable theEntity,
365                        ResourceSearchParams theActiveSearchParams) {
366                findMissingSearchParams(
367                                thePartitionSettings,
368                                theStorageSettings,
369                                theEntity,
370                                theActiveSearchParams,
371                                RestSearchParameterTypeEnum.STRING,
372                                myStringParams);
373                findMissingSearchParams(
374                                thePartitionSettings,
375                                theStorageSettings,
376                                theEntity,
377                                theActiveSearchParams,
378                                RestSearchParameterTypeEnum.NUMBER,
379                                myNumberParams);
380                findMissingSearchParams(
381                                thePartitionSettings,
382                                theStorageSettings,
383                                theEntity,
384                                theActiveSearchParams,
385                                RestSearchParameterTypeEnum.QUANTITY,
386                                myQuantityParams);
387                findMissingSearchParams(
388                                thePartitionSettings,
389                                theStorageSettings,
390                                theEntity,
391                                theActiveSearchParams,
392                                RestSearchParameterTypeEnum.DATE,
393                                myDateParams);
394                findMissingSearchParams(
395                                thePartitionSettings,
396                                theStorageSettings,
397                                theEntity,
398                                theActiveSearchParams,
399                                RestSearchParameterTypeEnum.URI,
400                                myUriParams);
401                findMissingSearchParams(
402                                thePartitionSettings,
403                                theStorageSettings,
404                                theEntity,
405                                theActiveSearchParams,
406                                RestSearchParameterTypeEnum.TOKEN,
407                                myTokenParams);
408                findMissingSearchParams(
409                                thePartitionSettings,
410                                theStorageSettings,
411                                theEntity,
412                                theActiveSearchParams,
413                                RestSearchParameterTypeEnum.SPECIAL,
414                                myCoordsParams);
415        }
416
417        @SuppressWarnings("unchecked")
418        private <RT extends BaseResourceIndexedSearchParam> void findMissingSearchParams(
419                        PartitionSettings thePartitionSettings,
420                        StorageSettings theStorageSettings,
421                        ResourceTable theEntity,
422                        ResourceSearchParams activeSearchParams,
423                        RestSearchParameterTypeEnum type,
424                        Collection<RT> paramCollection) {
425                for (String nextParamName : activeSearchParams.getSearchParamNames()) {
426                        if (nextParamName == null || myIgnoredParams.contains(nextParamName)) {
427                                continue;
428                        }
429
430                        RuntimeSearchParam searchParam = activeSearchParams.get(nextParamName);
431                        if (RuntimeSearchParamHelper.isResourceLevel(searchParam)) {
432                                continue;
433                        }
434
435                        if (searchParam.getParamType() == type) {
436                                boolean haveParam = false;
437                                for (BaseResourceIndexedSearchParam nextParam : paramCollection) {
438                                        if (nextParam.getParamName().equals(nextParamName)) {
439                                                haveParam = true;
440                                                break;
441                                        }
442                                }
443
444                                if (!haveParam) {
445                                        BaseResourceIndexedSearchParam param;
446                                        switch (type) {
447                                                case DATE:
448                                                        param = new ResourceIndexedSearchParamDate();
449                                                        break;
450                                                case NUMBER:
451                                                        param = new ResourceIndexedSearchParamNumber();
452                                                        break;
453                                                case QUANTITY:
454                                                        param = new ResourceIndexedSearchParamQuantity();
455                                                        break;
456                                                case STRING:
457                                                        param = new ResourceIndexedSearchParamString().setStorageSettings(theStorageSettings);
458                                                        break;
459                                                case TOKEN:
460                                                        param = new ResourceIndexedSearchParamToken();
461                                                        break;
462                                                case URI:
463                                                        param = new ResourceIndexedSearchParamUri();
464                                                        break;
465                                                case SPECIAL:
466                                                        if (BaseSearchParamExtractor.COORDS_INDEX_PATHS.contains(searchParam.getPath())) {
467                                                                param = new ResourceIndexedSearchParamCoords();
468                                                                break;
469                                                        } else {
470                                                                continue;
471                                                        }
472                                                case COMPOSITE:
473                                                case HAS:
474                                                case REFERENCE:
475                                                default:
476                                                        continue;
477                                        }
478                                        param.setPartitionSettings(thePartitionSettings);
479                                        param.setResource(theEntity);
480                                        param.setMissing(true);
481                                        param.setParamName(nextParamName);
482                                        param.calculateHashes();
483                                        paramCollection.add((RT) param);
484                                }
485                        }
486                }
487        }
488
489        /**
490         * This method is used to create a set of all possible combinations of
491         * parameters across a set of search parameters. An example of why
492         * this is needed:
493         * <p>
494         * Let's say we have a unique index on (Patient:gender AND Patient:name).
495         * Then we pass in <code>SMITH, John</code> with a gender of <code>male</code>.
496         * </p>
497         * <p>
498         * In this case, because the name parameter matches both first and last name,
499         * we now need two unique indexes:
500         * <ul>
501         * <li>Patient?gender=male&amp;name=SMITH</li>
502         * <li>Patient?gender=male&amp;name=JOHN</li>
503         * </ul>
504         * </p>
505         * <p>
506         * So this recursive algorithm calculates those
507         * </p>
508         *
509         * @param theResourceType E.g. <code>Patient
510         * @param thePartsChoices E.g. <code>[[gender=male], [name=SMITH, name=JOHN]]</code>
511         */
512        public static Set<String> extractCompositeStringUniquesValueChains(
513                        String theResourceType, List<List<String>> thePartsChoices) {
514
515                for (List<String> next : thePartsChoices) {
516                        next.removeIf(StringUtils::isBlank);
517                        if (next.isEmpty()) {
518                                return Collections.emptySet();
519                        }
520                }
521
522                if (thePartsChoices.isEmpty()) {
523                        return Collections.emptySet();
524                }
525
526                thePartsChoices.sort((o1, o2) -> {
527                        String str1 = null;
528                        String str2 = null;
529                        if (o1.size() > 0) {
530                                str1 = o1.get(0);
531                        }
532                        if (o2.size() > 0) {
533                                str2 = o2.get(0);
534                        }
535                        return compare(str1, str2);
536                });
537
538                List<String> values = new ArrayList<>();
539                Set<String> queryStringsToPopulate = new HashSet<>();
540                extractCompositeStringUniquesValueChains(theResourceType, thePartsChoices, values, queryStringsToPopulate);
541
542                values.removeIf(StringUtils::isBlank);
543
544                return queryStringsToPopulate;
545        }
546
547        private static void extractCompositeStringUniquesValueChains(
548                        String theResourceType,
549                        List<List<String>> thePartsChoices,
550                        List<String> theValues,
551                        Set<String> theQueryStringsToPopulate) {
552                if (thePartsChoices.size() > 0) {
553                        List<String> nextList = thePartsChoices.get(0);
554                        Collections.sort(nextList);
555                        for (String nextChoice : nextList) {
556                                theValues.add(nextChoice);
557                                extractCompositeStringUniquesValueChains(
558                                                theResourceType,
559                                                thePartsChoices.subList(1, thePartsChoices.size()),
560                                                theValues,
561                                                theQueryStringsToPopulate);
562                                theValues.remove(theValues.size() - 1);
563                        }
564                } else {
565                        if (theValues.size() > 0) {
566                                StringBuilder uniqueString = new StringBuilder();
567                                uniqueString.append(theResourceType);
568
569                                for (int i = 0; i < theValues.size(); i++) {
570                                        uniqueString.append(i == 0 ? "?" : "&");
571                                        uniqueString.append(theValues.get(i));
572                                }
573
574                                theQueryStringsToPopulate.add(uniqueString.toString());
575                        }
576                }
577        }
578}