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.provider;
021
022import ca.uhn.fhir.context.support.ConceptValidationOptions;
023import ca.uhn.fhir.context.support.IValidationSupport;
024import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult;
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.config.JpaStorageSettings;
029import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
030import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet;
031import ca.uhn.fhir.jpa.config.JpaConfig;
032import ca.uhn.fhir.jpa.model.util.JpaConstants;
033import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
034import ca.uhn.fhir.rest.annotation.IdParam;
035import ca.uhn.fhir.rest.annotation.Operation;
036import ca.uhn.fhir.rest.annotation.OperationParam;
037import ca.uhn.fhir.rest.api.server.RequestDetails;
038import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
039import ca.uhn.fhir.rest.server.provider.ProviderConstants;
040import ca.uhn.fhir.util.ParametersUtil;
041import com.google.common.annotations.VisibleForTesting;
042import jakarta.servlet.http.HttpServletRequest;
043import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
044import org.hl7.fhir.instance.model.api.IBaseCoding;
045import org.hl7.fhir.instance.model.api.IBaseParameters;
046import org.hl7.fhir.instance.model.api.IBaseResource;
047import org.hl7.fhir.instance.model.api.ICompositeType;
048import org.hl7.fhir.instance.model.api.IIdType;
049import org.hl7.fhir.instance.model.api.IPrimitiveType;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052import org.springframework.beans.factory.annotation.Autowired;
053import org.springframework.beans.factory.annotation.Qualifier;
054
055import java.util.Optional;
056import java.util.function.Supplier;
057
058import static org.apache.commons.lang3.StringUtils.isNotBlank;
059
060public class ValueSetOperationProvider extends BaseJpaProvider {
061
062        private static final Logger ourLog = LoggerFactory.getLogger(ValueSetOperationProvider.class);
063
064        @Autowired
065        protected IValidationSupport myValidationSupport;
066
067        @Autowired
068        private DaoRegistry myDaoRegistry;
069
070        @Autowired
071        private ITermReadSvc myTermReadSvc;
072
073        @Autowired
074        @Qualifier(JpaConfig.JPA_VALIDATION_SUPPORT_CHAIN)
075        private ValidationSupportChain myValidationSupportChain;
076
077        @VisibleForTesting
078        public void setDaoRegistryForUnitTest(DaoRegistry theDaoRegistry) {
079                myDaoRegistry = theDaoRegistry;
080        }
081
082        public void setValidationSupport(IValidationSupport theValidationSupport) {
083                myValidationSupport = theValidationSupport;
084        }
085
086        @Operation(name = JpaConstants.OPERATION_EXPAND, idempotent = true, typeName = "ValueSet")
087        public IBaseResource expand(
088                        HttpServletRequest theServletRequest,
089                        @IdParam(optional = true) IIdType theId,
090                        @OperationParam(name = "valueSet", min = 0, max = 1) IBaseResource theValueSet,
091                        @OperationParam(name = "url", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theUrl,
092                        @OperationParam(name = "valueSetVersion", min = 0, max = 1, typeName = "string")
093                                        IPrimitiveType<String> theValueSetVersion,
094                        @OperationParam(name = "filter", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theFilter,
095                        @OperationParam(name = "context", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theContext,
096                        @OperationParam(name = "contextDirection", min = 0, max = 1, typeName = "string")
097                                        IPrimitiveType<String> theContextDirection,
098                        @OperationParam(name = "offset", min = 0, max = 1, typeName = "integer") IPrimitiveType<Integer> theOffset,
099                        @OperationParam(name = "count", min = 0, max = 1, typeName = "integer") IPrimitiveType<Integer> theCount,
100                        @OperationParam(
101                                                        name = JpaConstants.OPERATION_EXPAND_PARAM_DISPLAY_LANGUAGE,
102                                                        min = 0,
103                                                        max = 1,
104                                                        typeName = "code")
105                                        IPrimitiveType<String> theDisplayLanguage,
106                        @OperationParam(
107                                                        name = JpaConstants.OPERATION_EXPAND_PARAM_INCLUDE_HIERARCHY,
108                                                        min = 0,
109                                                        max = 1,
110                                                        typeName = "boolean")
111                                        IPrimitiveType<Boolean> theIncludeHierarchy,
112                        RequestDetails theRequestDetails) {
113
114                startRequest(theServletRequest);
115                try {
116
117                        return getDao().expand(
118                                                        theId,
119                                                        theValueSet,
120                                                        theUrl,
121                                                        theValueSetVersion,
122                                                        theFilter,
123                                                        theContext,
124                                                        theContextDirection,
125                                                        theOffset,
126                                                        theCount,
127                                                        theDisplayLanguage,
128                                                        theIncludeHierarchy,
129                                                        theRequestDetails);
130
131                } finally {
132                        endRequest(theServletRequest);
133                }
134        }
135
136        @SuppressWarnings("unchecked")
137        protected IFhirResourceDaoValueSet<IBaseResource> getDao() {
138                return (IFhirResourceDaoValueSet<IBaseResource>) myDaoRegistry.getResourceDao("ValueSet");
139        }
140
141        @SuppressWarnings("unchecked")
142        @Operation(
143                        name = JpaConstants.OPERATION_VALIDATE_CODE,
144                        idempotent = true,
145                        typeName = "ValueSet",
146                        returnParameters = {
147                                @OperationParam(name = CodeValidationResult.RESULT, typeName = "boolean", min = 1),
148                                @OperationParam(name = CodeValidationResult.MESSAGE, typeName = "string"),
149                                @OperationParam(name = CodeValidationResult.DISPLAY, typeName = "string"),
150                                @OperationParam(name = CodeValidationResult.SOURCE_DETAILS, typeName = "string")
151                        })
152        public IBaseParameters validateCode(
153                        HttpServletRequest theServletRequest,
154                        @IdParam(optional = true) IIdType theId,
155                        @OperationParam(name = "url", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theValueSetUrl,
156                        @OperationParam(name = "valueSetVersion", min = 0, max = 1, typeName = "string")
157                                        IPrimitiveType<String> theValueSetVersion,
158                        @OperationParam(name = "code", min = 0, max = 1, typeName = "code") IPrimitiveType<String> theCode,
159                        @OperationParam(name = "system", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theSystem,
160                        @OperationParam(name = "systemVersion", min = 0, max = 1, typeName = "string")
161                                        IPrimitiveType<String> theSystemVersion,
162                        @OperationParam(name = CodeValidationResult.DISPLAY, min = 0, max = 1, typeName = "string")
163                                        IPrimitiveType<String> theDisplay,
164                        @OperationParam(name = "coding", min = 0, max = 1, typeName = "Coding") IBaseCoding theCoding,
165                        @OperationParam(name = "codeableConcept", min = 0, max = 1, typeName = "CodeableConcept")
166                                        ICompositeType theCodeableConcept,
167                        RequestDetails theRequestDetails) {
168
169                CodeValidationResult result;
170                startRequest(theServletRequest);
171                try {
172                        // If a Remote Terminology Server has been configured, use it
173                        if (myValidationSupportChain != null && myValidationSupportChain.isRemoteTerminologyServiceConfigured()) {
174                                String theSystemString =
175                                                (theSystem != null && theSystem.hasValue()) ? theSystem.getValueAsString() : null;
176                                String theCodeString = (theCode != null && theCode.hasValue()) ? theCode.getValueAsString() : null;
177                                String theDisplayString =
178                                                (theDisplay != null && theDisplay.hasValue()) ? theDisplay.getValueAsString() : null;
179                                String theValueSetUrlString = (theValueSetUrl != null && theValueSetUrl.hasValue())
180                                                ? theValueSetUrl.getValueAsString()
181                                                : null;
182                                if (theCoding != null) {
183                                        if (isNotBlank(theCoding.getSystem())) {
184                                                if (theSystemString != null && !theSystemString.equalsIgnoreCase(theCoding.getSystem())) {
185                                                        throw new InvalidRequestException(Msg.code(2352) + "Coding.system '" + theCoding.getSystem()
186                                                                        + "' does not equal param system '" + theSystemString
187                                                                        + "'. Unable to validate-code.");
188                                                }
189                                                theSystemString = theCoding.getSystem();
190                                                theCodeString = theCoding.getCode();
191                                                theDisplayString = theCoding.getDisplay();
192                                        }
193                                }
194
195                                result = validateCodeWithTerminologyService(
196                                                                theSystemString, theCodeString, theDisplayString, theValueSetUrlString)
197                                                .orElseGet(supplyUnableToValidateResult(theSystemString, theCodeString, theValueSetUrlString));
198                        } else {
199                                // Otherwise, use the local DAO layer to validate the code
200                                IFhirResourceDaoValueSet<IBaseResource> dao = getDao();
201                                IPrimitiveType<String> valueSetIdentifier;
202                                if (theValueSetUrl != null && theValueSetVersion != null) {
203                                        valueSetIdentifier = (IPrimitiveType<String>)
204                                                        getContext().getElementDefinition("uri").newInstance();
205                                        valueSetIdentifier.setValue(theValueSetUrl.getValue() + "|" + theValueSetVersion);
206                                } else {
207                                        valueSetIdentifier = theValueSetUrl;
208                                }
209                                IPrimitiveType<String> codeSystemIdentifier;
210                                if (theSystem != null && theSystemVersion != null) {
211                                        codeSystemIdentifier = (IPrimitiveType<String>)
212                                                        getContext().getElementDefinition("uri").newInstance();
213                                        codeSystemIdentifier.setValue(theSystem.getValue() + "|" + theSystemVersion);
214                                } else {
215                                        codeSystemIdentifier = theSystem;
216                                }
217                                result = dao.validateCode(
218                                                valueSetIdentifier,
219                                                theId,
220                                                theCode,
221                                                codeSystemIdentifier,
222                                                theDisplay,
223                                                theCoding,
224                                                theCodeableConcept,
225                                                theRequestDetails);
226                        }
227                        return result.toParameters(getContext());
228                } finally {
229                        endRequest(theServletRequest);
230                }
231        }
232
233        private Optional<CodeValidationResult> validateCodeWithTerminologyService(
234                        String theSystem, String theCode, String theDisplay, String theValueSetUrl) {
235                return Optional.ofNullable(myValidationSupportChain.validateCode(
236                                new ValidationSupportContext(myValidationSupportChain),
237                                new ConceptValidationOptions(),
238                                theSystem,
239                                theCode,
240                                theDisplay,
241                                theValueSetUrl));
242        }
243
244        private Supplier<CodeValidationResult> supplyUnableToValidateResult(
245                        String theSystem, String theCode, String theValueSetUrl) {
246                return () -> new CodeValidationResult()
247                                .setMessage("Validator is unable to provide validation for " + theCode + "#" + theSystem
248                                                + " - Unknown or unusable ValueSet[" + theValueSetUrl + "]");
249        }
250
251        @Operation(
252                        name = ProviderConstants.OPERATION_INVALIDATE_EXPANSION,
253                        idempotent = false,
254                        typeName = "ValueSet",
255                        returnParameters = {
256                                @OperationParam(name = CodeValidationResult.MESSAGE, typeName = "string", min = 1, max = 1)
257                        })
258        public IBaseParameters invalidateValueSetExpansion(
259                        @IdParam IIdType theValueSetId, RequestDetails theRequestDetails, HttpServletRequest theServletRequest) {
260                startRequest(theServletRequest);
261                try {
262
263                        String outcome = myTermReadSvc.invalidatePreCalculatedExpansion(theValueSetId, theRequestDetails);
264
265                        IBaseParameters retVal = ParametersUtil.newInstance(getContext());
266                        ParametersUtil.addParameterToParametersString(getContext(), retVal, CodeValidationResult.MESSAGE, outcome);
267                        return retVal;
268
269                } finally {
270                        endRequest(theServletRequest);
271                }
272        }
273
274        public static ValueSetExpansionOptions createValueSetExpansionOptions(
275                        JpaStorageSettings theStorageSettings,
276                        IPrimitiveType<Integer> theOffset,
277                        IPrimitiveType<Integer> theCount,
278                        IPrimitiveType<Boolean> theIncludeHierarchy,
279                        IPrimitiveType<String> theFilter,
280                        IPrimitiveType<String> theDisplayLanguage) {
281                int offset = theStorageSettings.getPreExpandValueSetsDefaultOffset();
282                if (theOffset != null && theOffset.hasValue()) {
283                        if (theOffset.getValue() >= 0) {
284                                offset = theOffset.getValue();
285                        } else {
286                                throw new InvalidRequestException(
287                                                Msg.code(1135) + "offset parameter for $expand operation must be >= 0 when specified. offset: "
288                                                                + theOffset.getValue());
289                        }
290                }
291
292                int count = theStorageSettings.getPreExpandValueSetsDefaultCount();
293                if (theCount != null && theCount.hasValue()) {
294                        if (theCount.getValue() >= 0) {
295                                count = theCount.getValue();
296                        } else {
297                                throw new InvalidRequestException(
298                                                Msg.code(1136) + "count parameter for $expand operation must be >= 0 when specified. count: "
299                                                                + theCount.getValue());
300                        }
301                }
302                int countMax = theStorageSettings.getPreExpandValueSetsMaxCount();
303                if (count > countMax) {
304                        ourLog.warn(
305                                        "count parameter for $expand operation of {} exceeds maximum value of {}; using maximum value.",
306                                        count,
307                                        countMax);
308                        count = countMax;
309                }
310
311                ValueSetExpansionOptions options = ValueSetExpansionOptions.forOffsetAndCount(offset, count);
312
313                if (theIncludeHierarchy != null && Boolean.TRUE.equals(theIncludeHierarchy.getValue())) {
314                        options.setIncludeHierarchy(true);
315                }
316
317                if (theFilter != null) {
318                        options.setFilter(theFilter.getValue());
319                }
320
321                if (theDisplayLanguage != null) {
322                        options.setTheDisplayLanguage(theDisplayLanguage.getValue());
323                }
324
325                return options;
326        }
327}