001/*
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 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.dao;
021
022import ca.uhn.fhir.context.FhirVersionEnum;
023import ca.uhn.fhir.context.support.ConceptValidationOptions;
024import ca.uhn.fhir.context.support.IValidationSupport;
025import ca.uhn.fhir.context.support.ValidationSupportContext;
026import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
027import ca.uhn.fhir.i18n.Msg;
028import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet;
029import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
030import ca.uhn.fhir.jpa.model.entity.ResourceTable;
031import ca.uhn.fhir.jpa.search.autocomplete.ValueSetAutocompleteOptions;
032import ca.uhn.fhir.jpa.util.LogicUtil;
033import ca.uhn.fhir.rest.api.server.RequestDetails;
034import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
035import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
036import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
037import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
038import ca.uhn.hapi.converters.canonical.VersionCanonicalizer;
039import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
040import org.hl7.fhir.instance.model.api.IBaseCoding;
041import org.hl7.fhir.instance.model.api.IBaseDatatype;
042import org.hl7.fhir.instance.model.api.IBaseResource;
043import org.hl7.fhir.instance.model.api.IIdType;
044import org.hl7.fhir.instance.model.api.IPrimitiveType;
045import org.hl7.fhir.r4.model.CodeableConcept;
046import org.hl7.fhir.r4.model.Coding;
047import org.hl7.fhir.r4.model.ValueSet;
048import org.springframework.beans.factory.annotation.Autowired;
049
050import java.util.Date;
051
052import static ca.uhn.fhir.jpa.dao.JpaResourceDaoCodeSystem.createVersionedSystemIfVersionIsPresent;
053import static ca.uhn.fhir.jpa.provider.ValueSetOperationProvider.createValueSetExpansionOptions;
054import static ca.uhn.fhir.util.DatatypeUtil.toStringValue;
055import static org.apache.commons.lang3.StringUtils.isNotBlank;
056
057public class JpaResourceDaoValueSet<T extends IBaseResource> extends BaseHapiFhirResourceDao<T>
058                implements IFhirResourceDaoValueSet<T> {
059        @Autowired
060        private IValidationSupport myValidationSupport;
061
062        @Autowired
063        private VersionCanonicalizer myVersionCanonicalizer;
064
065        @Autowired(required = false)
066        private IFulltextSearchSvc myFulltextSearch;
067
068        @Override
069        public T expand(IIdType theId, ValueSetExpansionOptions theOptions, RequestDetails theRequestDetails) {
070                T source = read(theId, theRequestDetails);
071                return expand(source, theOptions);
072        }
073
074        @SuppressWarnings("unchecked")
075        @Override
076        public T expandByIdentifier(String theUri, ValueSetExpansionOptions theOptions) {
077                IValidationSupport.ValueSetExpansionOutcome expansionOutcome = myValidationSupport.expandValueSet(
078                                new ValidationSupportContext(myValidationSupport), theOptions, theUri);
079                return extractValueSetOrThrowException(expansionOutcome);
080        }
081
082        @Override
083        public T expand(T theSource, ValueSetExpansionOptions theOptions) {
084                IValidationSupport.ValueSetExpansionOutcome expansionOutcome = myValidationSupport.expandValueSet(
085                                new ValidationSupportContext(myValidationSupport), theOptions, theSource);
086                return extractValueSetOrThrowException(expansionOutcome);
087        }
088
089        @Override
090        public T expand(
091                        IIdType theId,
092                        T theValueSet,
093                        IPrimitiveType<String> theUrl,
094                        IPrimitiveType<String> theValueSetVersion,
095                        IPrimitiveType<String> theFilter,
096                        IPrimitiveType<String> theContext,
097                        IPrimitiveType<String> theContextDirection,
098                        IPrimitiveType<Integer> theOffset,
099                        IPrimitiveType<Integer> theCount,
100                        IPrimitiveType<String> theDisplayLanguage,
101                        IPrimitiveType<Boolean> theIncludeHierarchy,
102                        RequestDetails theRequestDetails) {
103                boolean haveId = theId != null && theId.hasIdPart();
104                boolean haveIdentifier = theUrl != null && isNotBlank(theUrl.getValue());
105                boolean haveValueSet = theValueSet != null && !theValueSet.isEmpty();
106                boolean haveValueSetVersion = theValueSetVersion != null && !theValueSetVersion.isEmpty();
107                boolean haveContextDirection = theContextDirection != null && !theContextDirection.isEmpty();
108                boolean haveContext = theContext != null && !theContext.isEmpty();
109
110                boolean isAutocompleteExtension =
111                                haveContext && haveContextDirection && "existing".equals(theContextDirection.getValue());
112
113                if (isAutocompleteExtension) {
114                        // this is a funky extension for NIH.  Do our own thing and return.
115                        ValueSetAutocompleteOptions options = ValueSetAutocompleteOptions.validateAndParseOptions(
116                                        myStorageSettings, theContext, theFilter, theCount, theId, theUrl, theValueSet);
117                        if (myFulltextSearch == null || myFulltextSearch.isDisabled()) {
118                                throw new InvalidRequestException(
119                                                Msg.code(2083)
120                                                                + " Autocomplete is not supported on this server, as the fulltext search service is not configured.");
121                        } else {
122                                return (T) myFulltextSearch.tokenAutocompleteValueSetSearch(options);
123                        }
124                }
125
126                if (!haveId && !haveIdentifier && !haveValueSet) {
127                        if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) {
128                                // "url" parameter is called "identifier" in DSTU2
129                                throw new InvalidRequestException(
130                                                Msg.code(1130)
131                                                                + "$expand operation at the type level (no ID specified) requires an identifier or a valueSet as a part of the request");
132                        }
133                        throw new InvalidRequestException(
134                                        Msg.code(1133)
135                                                        + "$expand operation at the type level (no ID specified) requires a url or a valueSet as a part of the request.");
136                }
137
138                if (!LogicUtil.multiXor(haveId, haveIdentifier, haveValueSet)) {
139                        if (myFhirContext.getVersion().getVersion() == FhirVersionEnum.DSTU2) {
140                                // "url" parameter is called "identifier" in DSTU2
141                                throw new InvalidRequestException(
142                                                Msg.code(1131)
143                                                                + "$expand must EITHER be invoked at the type level, or have an identifier specified, or have a ValueSet specified. Can not combine these options.");
144                        }
145                        throw new InvalidRequestException(
146                                        Msg.code(1134)
147                                                        + "$expand must EITHER be invoked at the instance level, or have a url specified, or have a ValueSet specified. Can not combine these options.");
148                }
149
150                ValueSetExpansionOptions options = createValueSetExpansionOptions(
151                                myStorageSettings, theOffset, theCount, theIncludeHierarchy, theFilter, theDisplayLanguage);
152
153                IValidationSupport.ValueSetExpansionOutcome outcome;
154                if (haveId) {
155                        IBaseResource valueSet = read(theId, theRequestDetails);
156                        outcome = myValidationSupport.expandValueSet(
157                                        new ValidationSupportContext(myValidationSupport), options, valueSet);
158                } else if (haveIdentifier) {
159                        String url;
160                        if (haveValueSetVersion) {
161                                url = theUrl.getValue() + "|" + theValueSetVersion.getValue();
162                        } else {
163                                url = theUrl.getValue();
164                        }
165                        outcome =
166                                        myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), options, url);
167                } else {
168                        outcome = myValidationSupport.expandValueSet(
169                                        new ValidationSupportContext(myValidationSupport), options, theValueSet);
170                }
171
172                return extractValueSetOrThrowException(outcome);
173        }
174
175        @SuppressWarnings("unchecked")
176        private T extractValueSetOrThrowException(IValidationSupport.ValueSetExpansionOutcome outcome) {
177                if (outcome == null) {
178                        throw new InternalErrorException(
179                                        Msg.code(2028) + "No validation support module was able to expand the given valueset");
180                }
181
182                if (outcome.getError() != null) {
183                        throw new PreconditionFailedException(Msg.code(2029) + outcome.getError());
184                }
185
186                return (T) outcome.getValueSet();
187        }
188
189        @Override
190        public IValidationSupport.CodeValidationResult validateCode(
191                        IPrimitiveType<String> theValueSetIdentifier,
192                        IIdType theValueSetId,
193                        IPrimitiveType<String> theCode,
194                        IPrimitiveType<String> theSystem,
195                        IPrimitiveType<String> theDisplay,
196                        IBaseCoding theCoding,
197                        IBaseDatatype theCodeableConcept,
198                        RequestDetails theRequestDetails) {
199
200                CodeableConcept codeableConcept = myVersionCanonicalizer.codeableConceptToCanonical(theCodeableConcept);
201                boolean haveCodeableConcept =
202                                codeableConcept != null && codeableConcept.getCoding().size() > 0;
203
204                Coding canonicalCodingToValidate = myVersionCanonicalizer.codingToCanonical((IBaseCoding) theCoding);
205                boolean haveCoding = canonicalCodingToValidate != null && !canonicalCodingToValidate.isEmpty();
206
207                boolean haveCode = theCode != null && !theCode.isEmpty();
208
209                if (!haveCodeableConcept && !haveCoding && !haveCode) {
210                        throw new InvalidRequestException(
211                                        Msg.code(899) + "No code, coding, or codeableConcept provided to validate");
212                }
213                if (!LogicUtil.multiXor(haveCodeableConcept, haveCoding, haveCode)) {
214                        throw new InvalidRequestException(Msg.code(900)
215                                        + "$validate-code can only validate (system AND code) OR (coding) OR (codeableConcept)");
216                }
217
218                String valueSetIdentifier;
219                if (theValueSetId != null) {
220                        IBaseResource valueSet = read(theValueSetId, theRequestDetails);
221                        StringBuilder valueSetIdentifierBuilder =
222                                        new StringBuilder(CommonCodeSystemsTerminologyService.getValueSetUrl(myFhirContext, valueSet));
223                        String valueSetVersion = CommonCodeSystemsTerminologyService.getValueSetVersion(myFhirContext, valueSet);
224                        if (valueSetVersion != null) {
225                                valueSetIdentifierBuilder.append("|").append(valueSetVersion);
226                        }
227                        valueSetIdentifier = valueSetIdentifierBuilder.toString();
228                } else if (isNotBlank(toStringValue(theValueSetIdentifier))) {
229                        valueSetIdentifier = toStringValue(theValueSetIdentifier);
230                } else {
231                        throw new InvalidRequestException(
232                                        Msg.code(901)
233                                                        + "Either ValueSet ID or ValueSet identifier or system and code must be provided. Unable to validate.");
234                }
235
236                if (haveCodeableConcept) {
237                        IValidationSupport.CodeValidationResult anyValidation = null;
238                        for (int i = 0; i < codeableConcept.getCoding().size(); i++) {
239                                Coding nextCoding = codeableConcept.getCoding().get(i);
240                                String system =
241                                                createVersionedSystemIfVersionIsPresent(nextCoding.getSystem(), nextCoding.getVersion());
242                                String code = nextCoding.getCode();
243                                String display = nextCoding.getDisplay();
244
245                                IValidationSupport.CodeValidationResult nextValidation =
246                                                validateCode(system, code, display, valueSetIdentifier);
247                                anyValidation = nextValidation;
248                                if (nextValidation.isOk()) {
249                                        return nextValidation;
250                                }
251                        }
252                        return anyValidation;
253                } else if (haveCoding) {
254                        String system = createVersionedSystemIfVersionIsPresent(
255                                        canonicalCodingToValidate.getSystem(), canonicalCodingToValidate.getVersion());
256                        String code = canonicalCodingToValidate.getCode();
257                        String display = canonicalCodingToValidate.getDisplay();
258                        return validateCode(system, code, display, valueSetIdentifier);
259                } else {
260                        String system = toStringValue(theSystem);
261                        String code = toStringValue(theCode);
262                        String display = toStringValue(theDisplay);
263                        return validateCode(system, code, display, valueSetIdentifier);
264                }
265        }
266
267        private IValidationSupport.CodeValidationResult validateCode(
268                        String theSystem, String theCode, String theDisplay, String theValueSetIdentifier) {
269                ValidationSupportContext context = new ValidationSupportContext(myValidationSupport);
270                ConceptValidationOptions options = new ConceptValidationOptions();
271                options.setValidateDisplay(isNotBlank(theDisplay));
272                IValidationSupport.CodeValidationResult result = myValidationSupport.validateCode(
273                                context, options, theSystem, theCode, theDisplay, theValueSetIdentifier);
274
275                if (result == null) {
276                        result = new IValidationSupport.CodeValidationResult();
277                        result.setMessage("Validator is unable to provide validation for " + theCode + "#" + theSystem
278                                        + " - Unknown or unusable ValueSet[" + theValueSetIdentifier + "]");
279                }
280
281                return result;
282        }
283
284        @Override
285        public ResourceTable updateEntity(
286                        RequestDetails theRequestDetails,
287                        IBaseResource theResource,
288                        IBasePersistedResource theEntity,
289                        Date theDeletedTimestampOrNull,
290                        boolean thePerformIndexing,
291                        boolean theUpdateVersion,
292                        TransactionDetails theTransactionDetails,
293                        boolean theForceUpdate,
294                        boolean theCreateNewHistoryEntry) {
295                ResourceTable retVal = super.updateEntity(
296                                theRequestDetails,
297                                theResource,
298                                theEntity,
299                                theDeletedTimestampOrNull,
300                                thePerformIndexing,
301                                theUpdateVersion,
302                                theTransactionDetails,
303                                theForceUpdate,
304                                theCreateNewHistoryEntry);
305
306                if (thePerformIndexing) {
307                        if (getStorageSettings().isPreExpandValueSets() && !retVal.isUnchangedInCurrentOperation()) {
308                                if (retVal.getDeleted() == null) {
309                                        ValueSet valueSet = myVersionCanonicalizer.valueSetToCanonical(theResource);
310                                        myTerminologySvc.storeTermValueSet(retVal, valueSet);
311                                } else {
312                                        myTerminologySvc.deleteValueSetAndChildren(retVal);
313                                }
314                        }
315                }
316
317                return retVal;
318        }
319}