001package ca.uhn.fhir.jpa.dao.predicate;
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.i18n.Msg;
024import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
025import ca.uhn.fhir.context.BaseRuntimeDeclaredChildDefinition;
026import ca.uhn.fhir.context.RuntimeSearchParam;
027import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
028import ca.uhn.fhir.interceptor.model.RequestPartitionId;
029import ca.uhn.fhir.jpa.dao.LegacySearchBuilder;
030import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
031import ca.uhn.fhir.jpa.model.entity.ModelConfig;
032import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
033import ca.uhn.fhir.jpa.searchparam.extractor.BaseSearchParamExtractor;
034import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
035import ca.uhn.fhir.model.api.IQueryParameterType;
036import ca.uhn.fhir.model.base.composite.BaseCodingDt;
037import ca.uhn.fhir.model.base.composite.BaseIdentifierDt;
038import ca.uhn.fhir.rest.param.NumberParam;
039import ca.uhn.fhir.rest.param.TokenParam;
040import ca.uhn.fhir.rest.param.TokenParamModifier;
041import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
042import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
043import ca.uhn.fhir.util.FhirVersionIndependentConcept;
044import com.google.common.collect.Sets;
045import org.hibernate.query.criteria.internal.CriteriaBuilderImpl;
046import org.hibernate.query.criteria.internal.predicate.BooleanStaticAssertionPredicate;
047import org.springframework.beans.factory.annotation.Autowired;
048import org.springframework.context.annotation.Scope;
049import org.springframework.stereotype.Component;
050
051import javax.persistence.criteria.CriteriaBuilder;
052import javax.persistence.criteria.Expression;
053import javax.persistence.criteria.From;
054import javax.persistence.criteria.Path;
055import javax.persistence.criteria.Predicate;
056import java.util.ArrayList;
057import java.util.Collection;
058import java.util.Collections;
059import java.util.List;
060import java.util.Set;
061import java.util.stream.Collectors;
062
063import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
064import static org.apache.commons.lang3.StringUtils.isBlank;
065import static org.apache.commons.lang3.StringUtils.isNotBlank;
066
067@Component
068@Scope("prototype")
069public
070class PredicateBuilderToken extends BasePredicateBuilder implements IPredicateBuilder {
071        private final PredicateBuilder myPredicateBuilder;
072        @Autowired
073        private ITermReadSvc myTerminologySvc;
074        @Autowired
075        private ModelConfig myModelConfig;
076
077        public PredicateBuilderToken(LegacySearchBuilder theSearchBuilder, PredicateBuilder thePredicateBuilder) {
078                super(theSearchBuilder);
079                myPredicateBuilder = thePredicateBuilder;
080        }
081
082        @Override
083        public Predicate addPredicate(String theResourceName,
084                                                                                        RuntimeSearchParam theSearchParam,
085                                                                                        List<? extends IQueryParameterType> theList,
086                                                                                        SearchFilterParser.CompareOperation theOperation,
087                                                                                        RequestPartitionId theRequestPartitionId) {
088
089                if (theList.get(0).getMissing() != null) {
090                        From<?, ResourceIndexedSearchParamToken> join = myQueryStack.createJoin(SearchBuilderJoinEnum.TOKEN, theSearchParam.getName());
091                        addPredicateParamMissingForNonReference(theResourceName, theSearchParam.getName(), theList.get(0).getMissing(), join, theRequestPartitionId);
092                        return null;
093                }
094
095                List<Predicate> codePredicates = new ArrayList<>();
096
097                List<IQueryParameterType> tokens = new ArrayList<>();
098                for (IQueryParameterType nextOr : theList) {
099
100                        if (nextOr instanceof TokenParam) {
101                                TokenParam id = (TokenParam) nextOr;
102                                if (id.isText()) {
103
104                                        // Check whether the :text modifier is actually enabled here
105                                        boolean tokenTextIndexingEnabled = BaseSearchParamExtractor.tokenTextIndexingEnabledForSearchParam(myModelConfig, theSearchParam);
106                                        if (!tokenTextIndexingEnabled) {
107                                                String msg;
108                                                if (myModelConfig.isSuppressStringIndexingInTokens()) {
109                                                        msg = myContext.getLocalizer().getMessage(PredicateBuilderToken.class, "textModifierDisabledForServer");
110                                                } else {
111                                                        msg = myContext.getLocalizer().getMessage(PredicateBuilderToken.class, "textModifierDisabledForSearchParam");
112                                                }
113                                                throw new MethodNotAllowedException(Msg.code(1032) + msg);
114                                        }
115
116                                        myPredicateBuilder.addPredicateString(theResourceName, theSearchParam, theList, theOperation, theRequestPartitionId);
117                                        break;
118                                }
119                        }
120
121                        tokens.add(nextOr);
122                }
123
124                if (tokens.isEmpty()) {
125                        return null;
126                }
127
128                From<?, ResourceIndexedSearchParamToken> join = myQueryStack.createJoin(SearchBuilderJoinEnum.TOKEN, theSearchParam.getName());
129                addPartitionIdPredicate(theRequestPartitionId, join, codePredicates);
130
131                Collection<Predicate> singleCode = createPredicateToken(tokens, theResourceName, theSearchParam, myCriteriaBuilder, join, theOperation, theRequestPartitionId);
132                assert singleCode != null;
133                codePredicates.addAll(singleCode);
134
135                Predicate spPredicate = myCriteriaBuilder.or(toArray(codePredicates));
136
137                myQueryStack.addPredicateWithImplicitTypeSelection(spPredicate);
138
139                return spPredicate;
140        }
141
142        public Collection<Predicate> createPredicateToken(Collection<IQueryParameterType> theParameters,
143                                                                                                                                          String theResourceName,
144                                                                                                                                          RuntimeSearchParam theSearchParam,
145                                                                                                                                          CriteriaBuilder theBuilder,
146                                                                                                                                          From<?, ResourceIndexedSearchParamToken> theFrom,
147                                                                                                                                          RequestPartitionId theRequestPartitionId) {
148                return createPredicateToken(
149                        theParameters,
150                        theResourceName,
151                        theSearchParam,
152                        theBuilder,
153                        theFrom,
154                        null,
155                        theRequestPartitionId);
156        }
157
158        private Collection<Predicate> createPredicateToken(Collection<IQueryParameterType> theParameters,
159                                                                                                                                                String theResourceName,
160                                                                                                                                                RuntimeSearchParam theSearchParam,
161                                                                                                                                                CriteriaBuilder theBuilder,
162                                                                                                                                                From<?, ResourceIndexedSearchParamToken> theFrom,
163                                                                                                                                                SearchFilterParser.CompareOperation operation,
164                                                                                                                                                RequestPartitionId theRequestPartitionId) {
165                final List<FhirVersionIndependentConcept> codes = new ArrayList<>();
166                String paramName = theSearchParam.getName();
167
168                TokenParamModifier modifier = null;
169                for (IQueryParameterType nextParameter : theParameters) {
170
171                        String code;
172                        String system;
173                        if (nextParameter instanceof TokenParam) {
174                                TokenParam id = (TokenParam) nextParameter;
175                                system = id.getSystem();
176                                code = (id.getValue());
177                                modifier = id.getModifier();
178                        } else if (nextParameter instanceof BaseIdentifierDt) {
179                                BaseIdentifierDt id = (BaseIdentifierDt) nextParameter;
180                                system = id.getSystemElement().getValueAsString();
181                                code = (id.getValueElement().getValue());
182                        } else if (nextParameter instanceof BaseCodingDt) {
183                                BaseCodingDt id = (BaseCodingDt) nextParameter;
184                                system = id.getSystemElement().getValueAsString();
185                                code = (id.getCodeElement().getValue());
186                        } else if (nextParameter instanceof NumberParam) {
187                                NumberParam number = (NumberParam) nextParameter;
188                                system = null;
189                                code = number.getValueAsQueryToken(myContext);
190                        } else {
191                                throw new IllegalArgumentException(Msg.code(1033) + "Invalid token type: " + nextParameter.getClass());
192                        }
193
194                        if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
195                                throw new InvalidRequestException(Msg.code(1034) + "Parameter[" + paramName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system);
196                        }
197
198                        if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
199                                throw new InvalidRequestException(Msg.code(1035) + "Parameter[" + paramName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code);
200                        }
201
202                        /*
203                         * Process token modifiers (:in, :below, :above)
204                         */
205
206                        if (modifier == TokenParamModifier.IN) {
207                                codes.addAll(myTerminologySvc.expandValueSetIntoConceptList(null, code));
208                        } else if (modifier == TokenParamModifier.ABOVE) {
209                                system = determineSystemIfMissing(theSearchParam, code, system);
210                                validateHaveSystemAndCodeForToken(paramName, code, system);
211                                codes.addAll(myTerminologySvc.findCodesAbove(system, code));
212                        } else if (modifier == TokenParamModifier.BELOW) {
213                                system = determineSystemIfMissing(theSearchParam, code, system);
214                                validateHaveSystemAndCodeForToken(paramName, code, system);
215                                codes.addAll(myTerminologySvc.findCodesBelow(system, code));
216                        } else {
217                                codes.add(new FhirVersionIndependentConcept(system, code));
218                        }
219
220                }
221
222                List<FhirVersionIndependentConcept> sortedCodesList = codes
223                        .stream()
224                        .filter(t -> t.getCode() != null || t.getSystem() != null)
225                        .sorted()
226                        .distinct()
227                        .collect(Collectors.toList());
228
229                if (codes.isEmpty()) {
230                        // This will never match anything
231                        return Collections.singletonList(new BooleanStaticAssertionPredicate((CriteriaBuilderImpl) theBuilder, false));
232                }
233
234                List<Predicate> retVal = new ArrayList<>();
235
236                // System only
237                List<FhirVersionIndependentConcept> systemOnlyCodes = sortedCodesList.stream().filter(t -> isBlank(t.getCode())).collect(Collectors.toList());
238                if (!systemOnlyCodes.isEmpty()) {
239                        retVal.add(addPredicate(theResourceName, paramName, theBuilder, theFrom, systemOnlyCodes, modifier, SearchBuilderTokenModeEnum.SYSTEM_ONLY, theRequestPartitionId));
240                }
241
242                // Code only
243                List<FhirVersionIndependentConcept> codeOnlyCodes = sortedCodesList.stream().filter(t -> t.getSystem() == null).collect(Collectors.toList());
244                if (!codeOnlyCodes.isEmpty()) {
245                        retVal.add(addPredicate(theResourceName, paramName, theBuilder, theFrom, codeOnlyCodes, modifier, SearchBuilderTokenModeEnum.VALUE_ONLY, theRequestPartitionId));
246                }
247
248                // System and code
249                List<FhirVersionIndependentConcept> systemAndCodeCodes = sortedCodesList.stream().filter(t -> isNotBlank(t.getCode()) && t.getSystem() != null).collect(Collectors.toList());
250                if (!systemAndCodeCodes.isEmpty()) {
251                        retVal.add(addPredicate(theResourceName, paramName, theBuilder, theFrom, systemAndCodeCodes, modifier, SearchBuilderTokenModeEnum.SYSTEM_AND_VALUE, theRequestPartitionId));
252                }
253
254                return retVal;
255        }
256
257        private String determineSystemIfMissing(RuntimeSearchParam theSearchParam, String code, String theSystem) {
258                String retVal = theSystem;
259                if (retVal == null) {
260                        if (theSearchParam != null) {
261                                Set<String> valueSetUris = Sets.newHashSet();
262                                for (String nextPath : theSearchParam.getPathsSplit()) {
263                                        if (!nextPath.startsWith(myResourceType + ".")) {
264                                                continue;
265                                        }
266                                        BaseRuntimeChildDefinition def = myContext.newTerser().getDefinition(myResourceType, nextPath);
267                                        if (def instanceof BaseRuntimeDeclaredChildDefinition) {
268                                                String valueSet = ((BaseRuntimeDeclaredChildDefinition) def).getBindingValueSet();
269                                                if (isNotBlank(valueSet)) {
270                                                        valueSetUris.add(valueSet);
271                                                }
272                                        }
273                                }
274                                if (valueSetUris.size() == 1) {
275                                        String valueSet = valueSetUris.iterator().next();
276                                        ValueSetExpansionOptions options = new ValueSetExpansionOptions()
277                                                .setFailOnMissingCodeSystem(false);
278                                        List<FhirVersionIndependentConcept> candidateCodes = myTerminologySvc.expandValueSetIntoConceptList(options, valueSet);
279                                        for (FhirVersionIndependentConcept nextCandidate : candidateCodes) {
280                                                if (nextCandidate.getCode().equals(code)) {
281                                                        retVal = nextCandidate.getSystem();
282                                                        break;
283                                                }
284                                        }
285                                }
286                        }
287                }
288                return retVal;
289        }
290
291        private void validateHaveSystemAndCodeForToken(String theParamName, String theCode, String theSystem) {
292                String systemDesc = defaultIfBlank(theSystem, "(missing)");
293                String codeDesc = defaultIfBlank(theCode, "(missing)");
294                if (isBlank(theCode)) {
295                        String msg = myContext.getLocalizer().getMessage(LegacySearchBuilder.class, "invalidCodeMissingSystem", theParamName, systemDesc, codeDesc);
296                        throw new InvalidRequestException(Msg.code(1036) + msg);
297                }
298                if (isBlank(theSystem)) {
299                        String msg = myContext.getLocalizer().getMessage(LegacySearchBuilder.class, "invalidCodeMissingCode", theParamName, systemDesc, codeDesc);
300                        throw new InvalidRequestException(Msg.code(1037) + msg);
301                }
302        }
303
304        private Predicate addPredicate(String theResourceName, String theParamName, CriteriaBuilder theBuilder, From<?, ResourceIndexedSearchParamToken> theFrom, List<FhirVersionIndependentConcept> theTokens, TokenParamModifier theModifier, SearchBuilderTokenModeEnum theTokenMode, RequestPartitionId theRequestPartitionId) {
305                if (myDontUseHashesForSearch) {
306                        final Path<String> systemExpression = theFrom.get("mySystem");
307                        final Path<String> valueExpression = theFrom.get("myValue");
308
309                        List<Predicate> orPredicates = new ArrayList<>();
310                        switch (theTokenMode) {
311                                case SYSTEM_ONLY: {
312                                        List<String> systems = theTokens.stream().map(t -> t.getSystem()).collect(Collectors.toList());
313                                        Predicate orPredicate = systemExpression.in(systems);
314                                        orPredicates.add(orPredicate);
315                                        break;
316                                }
317                                case VALUE_ONLY:
318                                        List<String> codes = theTokens.stream().map(t -> t.getCode()).collect(Collectors.toList());
319                                        Predicate orPredicate = valueExpression.in(codes);
320                                        orPredicates.add(orPredicate);
321                                        break;
322                                case SYSTEM_AND_VALUE:
323                                        for (FhirVersionIndependentConcept next : theTokens) {
324                                                orPredicates.add(theBuilder.and(
325                                                        toEqualOrIsNullPredicate(systemExpression, next.getSystem()),
326                                                        toEqualOrIsNullPredicate(valueExpression, next.getCode())
327                                                ));
328                                        }
329                                        break;
330                        }
331
332                        Predicate or = theBuilder.or(orPredicates.toArray(new Predicate[0]));
333                        if (theModifier == TokenParamModifier.NOT) {
334                                or = theBuilder.not(or);
335                        }
336
337                        return combineParamIndexPredicateWithParamNamePredicate(theResourceName, theParamName, theFrom, or, theRequestPartitionId);
338                }
339
340                /*
341                 * Note: A null system value means "match any system", but
342                 * an empty-string system value means "match values that
343                 * explicitly have no system".
344                 */
345                Expression<Long> hashField;
346                List<Long> values;
347                switch (theTokenMode) {
348                        case SYSTEM_ONLY:
349                                hashField = theFrom.get("myHashSystem").as(Long.class);
350                                values = theTokens
351                                        .stream()
352                                        .map(t -> ResourceIndexedSearchParamToken.calculateHashSystem(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName, t.getSystem()))
353                                        .collect(Collectors.toList());
354                                break;
355                        case VALUE_ONLY:
356                                hashField = theFrom.get("myHashValue").as(Long.class);
357                                values = theTokens
358                                        .stream()
359                                        .map(t -> ResourceIndexedSearchParamToken.calculateHashValue(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName, t.getCode()))
360                                        .collect(Collectors.toList());
361                                break;
362                        case SYSTEM_AND_VALUE:
363                        default:
364                                hashField = theFrom.get("myHashSystemAndValue").as(Long.class);
365                                values = theTokens
366                                        .stream()
367                                        .map(t -> ResourceIndexedSearchParamToken.calculateHashSystemAndValue(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName, t.getSystem(), t.getCode()))
368                                        .collect(Collectors.toList());
369                                break;
370                }
371
372                /*
373                 * Note: At one point we had an IF-ELSE here that did an equals if there was only 1 value, and an IN if there
374                 * was more than 1. This caused a performance regression for some reason in Postgres though. So maybe simpler
375                 * is better..
376                 */
377                Predicate predicate = hashField.in(values);
378
379                if (theModifier == TokenParamModifier.NOT) {
380                        Predicate identityPredicate = theBuilder.equal(theFrom.get("myHashIdentity").as(Long.class), BaseResourceIndexedSearchParam.calculateHashIdentity(getPartitionSettings(), theRequestPartitionId, theResourceName, theParamName));
381                        Predicate disjunctionPredicate = theBuilder.not(predicate);
382                        predicate = theBuilder.and(identityPredicate, disjunctionPredicate);
383                }
384                return predicate;
385        }
386
387        private <T> Expression<Boolean> toEqualOrIsNullPredicate(Path<T> theExpression, T theCode) {
388                if (theCode == null) {
389                        return myCriteriaBuilder.isNull(theExpression);
390                }
391                return myCriteriaBuilder.equal(theExpression, theCode);
392        }
393}