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