001package ca.uhn.fhir.jpa.search.builder.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.context.BaseRuntimeChildDefinition;
024import ca.uhn.fhir.context.BaseRuntimeDeclaredChildDefinition;
025import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
026import ca.uhn.fhir.context.FhirContext;
027import ca.uhn.fhir.context.FhirVersionEnum;
028import ca.uhn.fhir.context.RuntimeResourceDefinition;
029import ca.uhn.fhir.context.RuntimeSearchParam;
030import ca.uhn.fhir.context.support.IValidationSupport;
031import ca.uhn.fhir.context.support.ValidationSupportContext;
032import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
033import ca.uhn.fhir.i18n.Msg;
034import ca.uhn.fhir.interceptor.model.RequestPartitionId;
035import ca.uhn.fhir.jpa.dao.LegacySearchBuilder;
036import ca.uhn.fhir.jpa.dao.predicate.SearchFilterParser;
037import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
038import ca.uhn.fhir.jpa.model.entity.ModelConfig;
039import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamToken;
040import ca.uhn.fhir.jpa.search.builder.QueryStack;
041import ca.uhn.fhir.jpa.search.builder.sql.SearchQueryBuilder;
042import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
043import ca.uhn.fhir.model.api.IQueryParameterType;
044import ca.uhn.fhir.model.base.composite.BaseCodingDt;
045import ca.uhn.fhir.model.base.composite.BaseIdentifierDt;
046import ca.uhn.fhir.rest.api.Constants;
047import ca.uhn.fhir.rest.param.NumberParam;
048import ca.uhn.fhir.rest.param.TokenParam;
049import ca.uhn.fhir.rest.param.TokenParamModifier;
050import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
051import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException;
052import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
053import ca.uhn.fhir.util.FhirVersionIndependentConcept;
054import ca.uhn.fhir.util.UrlUtil;
055import com.google.common.collect.Sets;
056import com.healthmarketscience.sqlbuilder.BinaryCondition;
057import com.healthmarketscience.sqlbuilder.Condition;
058import com.healthmarketscience.sqlbuilder.dbspec.basic.DbColumn;
059import org.hl7.fhir.instance.model.api.IBase;
060import org.hl7.fhir.instance.model.api.IBaseResource;
061import org.hl7.fhir.instance.model.api.IPrimitiveType;
062import org.springframework.beans.factory.annotation.Autowired;
063
064import java.util.ArrayList;
065import java.util.Arrays;
066import java.util.Collection;
067import java.util.List;
068import java.util.Optional;
069import java.util.Set;
070import java.util.stream.Collectors;
071
072import static ca.uhn.fhir.jpa.search.builder.QueryStack.toAndPredicate;
073import static ca.uhn.fhir.jpa.search.builder.QueryStack.toEqualToOrInPredicate;
074import static ca.uhn.fhir.jpa.search.builder.QueryStack.toOrPredicate;
075import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
076import static org.apache.commons.lang3.StringUtils.isBlank;
077import static org.apache.commons.lang3.StringUtils.isNotBlank;
078
079public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder {
080
081        private final DbColumn myColumnResId;
082        private final DbColumn myColumnHashSystemAndValue;
083        private final DbColumn myColumnHashSystem;
084        private final DbColumn myColumnHashValue;
085        private final DbColumn myColumnSystem;
086        private final DbColumn myColumnValue;
087
088        @Autowired
089        private IValidationSupport myValidationSupport;
090        @Autowired
091        private ITermReadSvc myTerminologySvc;
092        @Autowired
093        private ModelConfig myModelConfig;
094        @Autowired
095        private FhirContext myContext;
096
097        /**
098         * Constructor
099         */
100        public TokenPredicateBuilder(SearchQueryBuilder theSearchSqlBuilder) {
101                super(theSearchSqlBuilder, theSearchSqlBuilder.addTable("HFJ_SPIDX_TOKEN"));
102                myColumnResId = getTable().addColumn("RES_ID");
103                myColumnHashSystem = getTable().addColumn("HASH_SYS");
104                myColumnHashSystemAndValue = getTable().addColumn("HASH_SYS_AND_VALUE");
105                myColumnHashValue = getTable().addColumn("HASH_VALUE");
106                myColumnSystem = getTable().addColumn("SP_SYSTEM");
107                myColumnValue = getTable().addColumn("SP_VALUE");
108        }
109
110        @Override
111        public DbColumn getResourceIdColumn() {
112                return myColumnResId;
113        }
114
115        public Condition createPredicateToken(Collection<IQueryParameterType> theParameters,
116                                                                                                          String theResourceName,
117                                                                                                          String theSpnamePrefix,
118                                                                                                          RuntimeSearchParam theSearchParam,
119                                                                                                          RequestPartitionId theRequestPartitionId) {
120                return createPredicateToken(
121                        theParameters,
122                        theResourceName,
123                        theSpnamePrefix,
124                        theSearchParam,
125                        null,
126                        theRequestPartitionId);
127        }
128
129        public Condition createPredicateToken(Collection<IQueryParameterType> theParameters,
130                                                                                                          String theResourceName,
131                                                                                                          String theSpnamePrefix,
132                                                                                                          RuntimeSearchParam theSearchParam,
133                                                                                                          SearchFilterParser.CompareOperation theOperation,
134                                                                                                          RequestPartitionId theRequestPartitionId) {
135
136
137                final List<FhirVersionIndependentConcept> codes = new ArrayList<>();
138
139                String paramName = QueryStack.getParamNameWithPrefix(theSpnamePrefix, theSearchParam.getName());
140
141                SearchFilterParser.CompareOperation operation = theOperation;
142
143                TokenParamModifier modifier = null;
144                for (IQueryParameterType nextParameter : theParameters) {
145
146                        String code;
147                        String system;
148                        if (nextParameter instanceof TokenParam) {
149                                TokenParam id = (TokenParam) nextParameter;
150                                system = id.getSystem();
151                                code = id.getValue();
152                                modifier = id.getModifier();
153                        } else if (nextParameter instanceof BaseIdentifierDt) {
154                                BaseIdentifierDt id = (BaseIdentifierDt) nextParameter;
155                                system = id.getSystemElement().getValueAsString();
156                                code = (id.getValueElement().getValue());
157                        } else if (nextParameter instanceof BaseCodingDt) {
158                                BaseCodingDt id = (BaseCodingDt) nextParameter;
159                                system = id.getSystemElement().getValueAsString();
160                                code = (id.getCodeElement().getValue());
161                        } else if (nextParameter instanceof NumberParam) {
162                                NumberParam number = (NumberParam) nextParameter;
163                                system = null;
164                                code = number.getValueAsQueryToken(getFhirContext());
165                        } else {
166                                throw new IllegalArgumentException(Msg.code(1236) + "Invalid token type: " + nextParameter.getClass());
167                        }
168
169                        if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
170                                throw new InvalidRequestException(Msg.code(1237) + "Parameter[" + paramName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system);
171                        }
172
173                        if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) {
174                                throw new InvalidRequestException(Msg.code(1238) + "Parameter[" + paramName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code);
175                        }
176
177                        /*
178                         * Process token modifiers (:in, :below, :above)
179                         */
180
181                        if (modifier == TokenParamModifier.IN || modifier == TokenParamModifier.NOT_IN) {
182                                if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) {
183                                        IValidationSupport.ValueSetExpansionOutcome expanded = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), new ValueSetExpansionOptions(), code);
184                                        codes.addAll(extractValueSetCodes(expanded.getValueSet()));
185                                } else {
186                                        codes.addAll(myTerminologySvc.expandValueSetIntoConceptList(null, code));
187                                }
188                                if (modifier == TokenParamModifier.NOT_IN) {
189                                        operation = SearchFilterParser.CompareOperation.ne;
190                                }
191                        } else if (modifier == TokenParamModifier.ABOVE) {
192                                system = determineSystemIfMissing(theSearchParam, code, system);
193                                validateHaveSystemAndCodeForToken(paramName, code, system);
194                                codes.addAll(myTerminologySvc.findCodesAbove(system, code));
195                        } else if (modifier == TokenParamModifier.BELOW) {
196                                system = determineSystemIfMissing(theSearchParam, code, system);
197                                validateHaveSystemAndCodeForToken(paramName, code, system);
198                                codes.addAll(myTerminologySvc.findCodesBelow(system, code));
199                        } else if (modifier == TokenParamModifier.OF_TYPE) {
200                                if (!myModelConfig.isIndexIdentifierOfType()) {
201                                        throw new MethodNotAllowedException(Msg.code(2012) + "The :of-type modifier is not enabled on this server");
202                                }
203                                if (isBlank(system) || isBlank(code)) {
204                                        throw new InvalidRequestException(Msg.code(2013) + "Invalid parameter value for :of-type query");
205                                }
206                                int pipeIdx = code.indexOf('|');
207                                if (pipeIdx < 1 || pipeIdx == code.length() - 1) {
208                                        throw new InvalidRequestException(Msg.code(2014) + "Invalid parameter value for :of-type query");
209                                }
210
211                                paramName = paramName + Constants.PARAMQUALIFIER_TOKEN_OF_TYPE;
212                                codes.add(new FhirVersionIndependentConcept(system, code));
213                        } else {
214                                if (modifier == TokenParamModifier.NOT && operation == null) {
215                                        operation = SearchFilterParser.CompareOperation.ne;
216                                }
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                        setMatchNothing();
232                        return null;
233                }
234
235
236                Condition predicate;
237                if (operation == SearchFilterParser.CompareOperation.ne) {
238
239                        /*
240                         * For a token :not search, we look for index rows that have the right identity (i.e. it's the right resource and
241                         * param name) but not the actual provided token value.
242                         */
243
244                        long hashIdentity = BaseResourceIndexedSearchParam.calculateHashIdentity(getPartitionSettings(), theRequestPartitionId, theResourceName, paramName);
245                        Condition hashIdentityPredicate = BinaryCondition.equalTo(getColumnHashIdentity(), generatePlaceholder(hashIdentity));
246
247                        Condition hashValuePredicate = createPredicateOrList(theResourceName, paramName, sortedCodesList, false);
248                        predicate = toAndPredicate(hashIdentityPredicate, hashValuePredicate);
249
250                } else {
251
252                        predicate = createPredicateOrList(theResourceName, paramName, sortedCodesList, true);
253
254                }
255
256                return predicate;
257        }
258
259        private List<FhirVersionIndependentConcept> extractValueSetCodes(IBaseResource theValueSet) {
260                List<FhirVersionIndependentConcept> retVal = new ArrayList<>();
261
262                RuntimeResourceDefinition vsDef = myContext.getResourceDefinition("ValueSet");
263                BaseRuntimeChildDefinition expansionChild = vsDef.getChildByName("expansion");
264                Optional<IBase> expansionOpt = expansionChild.getAccessor().getFirstValueOrNull(theValueSet);
265                if (expansionOpt.isPresent()) {
266                        IBase expansion = expansionOpt.get();
267                        BaseRuntimeElementCompositeDefinition<?> expansionDef = (BaseRuntimeElementCompositeDefinition<?>) myContext.getElementDefinition(expansion.getClass());
268                        BaseRuntimeChildDefinition containsChild = expansionDef.getChildByName("contains");
269                        List<IBase> contains = containsChild.getAccessor().getValues(expansion);
270                                
271                        BaseRuntimeChildDefinition.IAccessor systemAccessor = null;
272                        BaseRuntimeChildDefinition.IAccessor codeAccessor = null;
273                        for (IBase nextContains : contains) {
274                                if (systemAccessor == null) {
275                                        systemAccessor = myContext.getElementDefinition(nextContains.getClass()).getChildByName("system").getAccessor();
276                                }
277                                if (codeAccessor == null) {
278                                        codeAccessor = myContext.getElementDefinition(nextContains.getClass()).getChildByName("code").getAccessor();
279                                }
280                                String system = systemAccessor
281                                        .getFirstValueOrNull(nextContains)
282                                        .map(t->(IPrimitiveType<?>)t)
283                                        .map(t->t.getValueAsString())
284                                        .orElse(null);
285                                String code = codeAccessor
286                                        .getFirstValueOrNull(nextContains)
287                                        .map(t->(IPrimitiveType<?>)t)
288                                        .map(t->t.getValueAsString())
289                                        .orElse(null);
290                                if (isNotBlank(system) && isNotBlank(code)) {
291                                        retVal.add(new FhirVersionIndependentConcept(system, code));
292                                }
293                        }
294                }
295
296                return retVal;
297        }
298
299        private String determineSystemIfMissing(RuntimeSearchParam theSearchParam, String code, String theSystem) {
300                String retVal = theSystem;
301                if (retVal == null) {
302                        if (theSearchParam != null) {
303                                Set<String> valueSetUris = Sets.newHashSet();
304                                for (String nextPath : theSearchParam.getPathsSplitForResourceType(getResourceType())) {
305                                        Class<? extends IBaseResource> type = getFhirContext().getResourceDefinition(getResourceType()).getImplementingClass();
306                                        BaseRuntimeChildDefinition def = getFhirContext().newTerser().getDefinition(type, nextPath);
307                                        if (def instanceof BaseRuntimeDeclaredChildDefinition) {
308                                                String valueSet = ((BaseRuntimeDeclaredChildDefinition) def).getBindingValueSet();
309                                                if (isNotBlank(valueSet)) {
310                                                        valueSetUris.add(valueSet);
311                                                }
312                                        }
313                                }
314                                if (valueSetUris.size() == 1) {
315                                        String valueSet = valueSetUris.iterator().next();
316                                        ValueSetExpansionOptions options = new ValueSetExpansionOptions()
317                                                .setFailOnMissingCodeSystem(false);
318                                        List<FhirVersionIndependentConcept> candidateCodes = myTerminologySvc.expandValueSetIntoConceptList(options, valueSet);
319                                        for (FhirVersionIndependentConcept nextCandidate : candidateCodes) {
320                                                if (nextCandidate.getCode().equals(code)) {
321                                                        retVal = nextCandidate.getSystem();
322                                                        break;
323                                                }
324                                        }
325                                }
326                        }
327                }
328                return retVal;
329        }
330
331        public DbColumn getColumnSystem() {
332                return myColumnSystem;
333        }
334
335        public DbColumn getColumnValue() {
336                return myColumnValue;
337        }
338
339        private void validateHaveSystemAndCodeForToken(String theParamName, String theCode, String theSystem) {
340                String systemDesc = defaultIfBlank(theSystem, "(missing)");
341                String codeDesc = defaultIfBlank(theCode, "(missing)");
342                if (isBlank(theCode)) {
343                        String msg = getFhirContext().getLocalizer().getMessage(LegacySearchBuilder.class, "invalidCodeMissingSystem", theParamName, systemDesc, codeDesc);
344                        throw new InvalidRequestException(Msg.code(1239) + msg);
345                }
346                if (isBlank(theSystem)) {
347                        String msg = getFhirContext().getLocalizer().getMessage(LegacySearchBuilder.class, "invalidCodeMissingCode", theParamName, systemDesc, codeDesc);
348                        throw new InvalidRequestException(Msg.code(1240) + msg);
349                }
350        }
351
352
353        private Condition createPredicateOrList(String theResourceType, String theSearchParamName, List<FhirVersionIndependentConcept> theCodes, boolean theWantEquals) {
354                Condition[] conditions = new Condition[theCodes.size()];
355
356                Long[] hashes = new Long[theCodes.size()];
357                DbColumn[] columns = new DbColumn[theCodes.size()];
358                boolean haveMultipleColumns = false;
359                for (int i = 0; i < conditions.length; i++) {
360
361                        FhirVersionIndependentConcept nextToken = theCodes.get(i);
362                        long hash;
363                        DbColumn column;
364                        if (nextToken.getSystem() == null) {
365                                hash = ResourceIndexedSearchParamToken.calculateHashValue(getPartitionSettings(), getRequestPartitionId(), theResourceType, theSearchParamName, nextToken.getCode());
366                                column = myColumnHashValue;
367                        } else if (isBlank(nextToken.getCode())) {
368                                hash = ResourceIndexedSearchParamToken.calculateHashSystem(getPartitionSettings(), getRequestPartitionId(), theResourceType, theSearchParamName, nextToken.getSystem());
369                                column = myColumnHashSystem;
370                        } else {
371                                hash = ResourceIndexedSearchParamToken.calculateHashSystemAndValue(getPartitionSettings(), getRequestPartitionId(), theResourceType, theSearchParamName, nextToken.getSystem(), nextToken.getCode());
372                                column = myColumnHashSystemAndValue;
373                        }
374                        hashes[i] = hash;
375                        columns[i] = column;
376                        if (i > 0 && columns[0] != columns[i]) {
377                                haveMultipleColumns = true;
378                        }
379                }
380
381                if (!haveMultipleColumns && conditions.length > 1) {
382                        List<Long> values = Arrays.asList(hashes);
383                        return toEqualToOrInPredicate(columns[0], generatePlaceholders(values), !theWantEquals);
384                }
385
386                for (int i = 0; i < conditions.length; i++) {
387                        String valuePlaceholder = generatePlaceholder(hashes[i]);
388                        if (theWantEquals) {
389                                conditions[i] = BinaryCondition.equalTo(columns[i], valuePlaceholder);
390                        } else {
391                                conditions[i] = BinaryCondition.notEqualTo(columns[i], valuePlaceholder);
392                        }
393                }
394                if (conditions.length > 1) {
395                        if (theWantEquals) {
396                                return toOrPredicate(conditions);
397                        } else {
398                                return toAndPredicate(conditions);
399                        }
400                } else {
401                        return conditions[0];
402                }
403        }
404}