001package ca.uhn.fhir.jpa.provider;
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.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.config.DaoConfig;
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.dao.IFulltextSearchSvc;
033import ca.uhn.fhir.jpa.model.util.JpaConstants;
034import ca.uhn.fhir.jpa.search.autocomplete.ValueSetAutocompleteOptions;
035import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
036import ca.uhn.fhir.rest.annotation.IdParam;
037import ca.uhn.fhir.rest.annotation.Operation;
038import ca.uhn.fhir.rest.annotation.OperationParam;
039import ca.uhn.fhir.rest.api.server.RequestDetails;
040import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
041import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
042import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
043import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
044import ca.uhn.fhir.rest.server.provider.ProviderConstants;
045import ca.uhn.fhir.util.ParametersUtil;
046import ca.uhn.fhir.util.UrlUtil;
047import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
048import org.hl7.fhir.instance.model.api.IBaseParameters;
049import org.hl7.fhir.instance.model.api.IBaseResource;
050import org.hl7.fhir.instance.model.api.ICompositeType;
051import org.hl7.fhir.instance.model.api.IIdType;
052import org.hl7.fhir.instance.model.api.IPrimitiveType;
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055import org.springframework.beans.factory.annotation.Autowired;
056import org.springframework.beans.factory.annotation.Qualifier;
057
058import javax.servlet.http.HttpServletRequest;
059
060import static org.apache.commons.lang3.StringUtils.isNotBlank;
061
062public class ValueSetOperationProvider extends BaseJpaProvider {
063
064        private static final Logger ourLog = LoggerFactory.getLogger(ValueSetOperationProvider.class);
065        @Autowired
066        private DaoConfig myDaoConfig;
067        @Autowired
068        private DaoRegistry myDaoRegistry;
069        @Autowired
070        private ITermReadSvc myTermReadSvc;
071        @Autowired
072        @Qualifier(JpaConfig.JPA_VALIDATION_SUPPORT_CHAIN)
073        private ValidationSupportChain myValidationSupportChain;
074        @Autowired
075        private IValidationSupport myValidationSupport;
076        @Autowired(required = false)
077        private IFulltextSearchSvc myFulltextSearch;
078
079        public void setValidationSupport(IValidationSupport theValidationSupport) {
080                myValidationSupport = theValidationSupport;
081        }
082
083        public void setDaoConfig(DaoConfig theDaoConfig) {
084                myDaoConfig = theDaoConfig;
085        }
086
087        public void setDaoRegistry(DaoRegistry theDaoRegistry) {
088                myDaoRegistry = theDaoRegistry;
089        }
090
091        public void setTermReadSvc(ITermReadSvc theTermReadSvc) {
092                myTermReadSvc = theTermReadSvc;
093        }
094
095        public void setValidationSupportChain(ValidationSupportChain theValidationSupportChain) {
096                myValidationSupportChain = theValidationSupportChain;
097        }
098
099        @Operation(name = JpaConstants.OPERATION_EXPAND, idempotent = true, typeName = "ValueSet")
100        public IBaseResource expand(
101                HttpServletRequest theServletRequest,
102                @IdParam(optional = true) IIdType theId,
103                @OperationParam(name = "valueSet", min = 0, max = 1) IBaseResource theValueSet,
104                @OperationParam(name = "url", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theUrl,
105                @OperationParam(name = "valueSetVersion", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theValueSetVersion,
106                @OperationParam(name = "filter", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theFilter,
107                @OperationParam(name = "context", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theContext,
108                @OperationParam(name = "contextDirection", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theContextDirection,
109                @OperationParam(name = "offset", min = 0, max = 1, typeName = "integer") IPrimitiveType<Integer> theOffset,
110                @OperationParam(name = "count", min = 0, max = 1, typeName = "integer") IPrimitiveType<Integer> theCount,
111                @OperationParam(name = JpaConstants.OPERATION_EXPAND_PARAM_INCLUDE_HIERARCHY, min = 0, max = 1, typeName = "boolean") IPrimitiveType<Boolean> theIncludeHierarchy,
112                RequestDetails theRequestDetails) {
113
114                boolean haveId = theId != null && theId.hasIdPart();
115                boolean haveIdentifier = theUrl != null && isNotBlank(theUrl.getValue());
116                boolean haveValueSet = theValueSet != null && !theValueSet.isEmpty();
117                boolean haveValueSetVersion = theValueSetVersion != null && !theValueSetVersion.isEmpty();
118                boolean haveContextDirection = theContextDirection != null && !theContextDirection.isEmpty();
119                boolean haveContext = theContext != null && !theContext.isEmpty();
120
121                boolean isAutocompleteExtension = haveContext && haveContextDirection && "existing".equals(theContextDirection.getValue());
122
123                if (isAutocompleteExtension) {
124                        // this is a funky extension for NIH.  Do our own thing and return.
125                        ValueSetAutocompleteOptions options = ValueSetAutocompleteOptions.validateAndParseOptions(myDaoConfig, theContext, theFilter, theCount, theId, theUrl, theValueSet);
126                        startRequest(theServletRequest);
127                        try {
128                                if (myFulltextSearch == null || myFulltextSearch.isDisabled()) {
129                                        throw new InvalidRequestException(Msg.code(2083) +  " Autocomplete is not supported on this server, as the fulltext search service is not configured.");
130                                } else {
131                                        return myFulltextSearch.tokenAutocompleteValueSetSearch(options);
132                                }
133                        } finally {
134                                endRequest(theServletRequest);
135                        }
136                }
137
138                if (!haveId && !haveIdentifier && !haveValueSet) {
139                        throw new InvalidRequestException(Msg.code(1133) + "$expand operation at the type level (no ID specified) requires a url or a valueSet as a part of the request.");
140                }
141
142                if (moreThanOneTrue(haveId, haveIdentifier, haveValueSet)) {
143                        throw new InvalidRequestException(Msg.code(1134) + "$expand must EITHER be invoked at the instance level, or have a url specified, or have a ValueSet specified. Can not combine these options.");
144                }
145
146                ValueSetExpansionOptions options = createValueSetExpansionOptions(myDaoConfig, theOffset, theCount, theIncludeHierarchy, theFilter);
147
148                startRequest(theServletRequest);
149                try {
150
151                        IFhirResourceDaoValueSet<IBaseResource, ICompositeType, ICompositeType> dao = getDao();
152
153                        IValidationSupport.ValueSetExpansionOutcome outcome;
154                        if (haveId) {
155                                IBaseResource valueSet = dao.read(theId, theRequestDetails);
156                                outcome = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), options, valueSet);
157                        } else if (haveIdentifier) {
158                                String url;
159                                if (haveValueSetVersion) {
160                                        url = theUrl.getValue() + "|" + theValueSetVersion.getValue();
161                                } else {
162                                        url = theUrl.getValue();
163                                }
164                                outcome = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), options, url);
165                        } else {
166                                outcome = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), options, theValueSet);
167                        }
168
169                        if (outcome == null) {
170                                throw new InternalErrorException(Msg.code(2028) + "No validation support module was able to expand the given valueset");
171                        }
172
173                        if (outcome.getError() != null) {
174                                throw new PreconditionFailedException(Msg.code(2029) + outcome.getError());
175                        }
176
177                        return outcome.getValueSet();
178
179                } finally {
180                        endRequest(theServletRequest);
181                }
182        }
183
184        @SuppressWarnings("unchecked")
185        private IFhirResourceDaoValueSet<IBaseResource, ICompositeType, ICompositeType> getDao() {
186                return (IFhirResourceDaoValueSet<IBaseResource, ICompositeType, ICompositeType>) myDaoRegistry.getResourceDao("ValueSet");
187        }
188
189        @SuppressWarnings("unchecked")
190        @Operation(name = JpaConstants.OPERATION_VALIDATE_CODE, idempotent = true, typeName = "ValueSet", returnParameters = {
191                @OperationParam(name = "result", typeName = "boolean", min = 1),
192                @OperationParam(name = "message", typeName = "string"),
193                @OperationParam(name = "display", typeName = "string")
194        })
195        public IBaseParameters validateCode(
196                HttpServletRequest theServletRequest,
197                @IdParam(optional = true) IIdType theId,
198                @OperationParam(name = "url", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theValueSetUrl,
199                @OperationParam(name = "valueSetVersion", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theValueSetVersion,
200                @OperationParam(name = "code", min = 0, max = 1) IPrimitiveType<String> theCode,
201                @OperationParam(name = "system", min = 0, max = 1, typeName = "uri") IPrimitiveType<String> theSystem,
202                @OperationParam(name = "systemVersion", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theSystemVersion,
203                @OperationParam(name = "display", min = 0, max = 1, typeName = "string") IPrimitiveType<String> theDisplay,
204                @OperationParam(name = "coding", min = 0, max = 1, typeName = "Coding") ICompositeType theCoding,
205                @OperationParam(name = "codeableConcept", min = 0, max = 1, typeName = "CodeableConcept") ICompositeType theCodeableConcept,
206                RequestDetails theRequestDetails
207        ) {
208
209                IValidationSupport.CodeValidationResult result;
210                startRequest(theServletRequest);
211                try {
212                        // If a Remote Terminology Server has been configured, use it
213                        if (myValidationSupportChain != null && myValidationSupportChain.isRemoteTerminologyServiceConfigured()) {
214                                String theSystemString = (theSystem != null && theSystem.hasValue()) ? theSystem.getValueAsString() : null;
215                                String theCodeString = (theCode != null && theCode.hasValue()) ? theCode.getValueAsString() : null;
216                                String theDisplayString = (theDisplay != null && theDisplay.hasValue()) ? theDisplay.getValueAsString() : null;
217                                String theValueSetUrlString = (theValueSetUrl != null && theValueSetUrl.hasValue()) ?
218                                        theValueSetUrl.getValueAsString() : null;
219                                result = myValidationSupportChain.validateCode(new ValidationSupportContext(myValidationSupportChain),
220                                        new ConceptValidationOptions(), theSystemString, theCodeString, theDisplayString, theValueSetUrlString);
221                        } else {
222                                // Otherwise, use the local DAO layer to validate the code
223                                IFhirResourceDaoValueSet<IBaseResource, ICompositeType, ICompositeType> dao = getDao();
224                                IPrimitiveType<String> valueSetIdentifier;
225                                if (theValueSetUrl != null && theValueSetVersion != null) {
226                                        valueSetIdentifier = (IPrimitiveType<String>) getContext().getElementDefinition("uri").newInstance();
227                                        valueSetIdentifier.setValue(theValueSetUrl.getValue() + "|" + theValueSetVersion);
228                                } else {
229                                        valueSetIdentifier = theValueSetUrl;
230                                }
231                                IPrimitiveType<String> codeSystemIdentifier;
232                                if (theSystem != null && theSystemVersion != null) {
233                                        codeSystemIdentifier = (IPrimitiveType<String>) getContext().getElementDefinition("uri").newInstance();
234                                        codeSystemIdentifier.setValue(theSystem.getValue() + "|" + theSystemVersion);
235                                } else {
236                                        codeSystemIdentifier = theSystem;
237                                }
238                                result = dao.validateCode(valueSetIdentifier, theId, theCode, codeSystemIdentifier, theDisplay, theCoding, theCodeableConcept, theRequestDetails);
239                        }
240                        return BaseJpaResourceProviderValueSetDstu2.toValidateCodeResult(getContext(), result);
241                } finally {
242                        endRequest(theServletRequest);
243                }
244        }
245
246        @Operation(name = ProviderConstants.OPERATION_INVALIDATE_EXPANSION, idempotent = false, typeName = "ValueSet", returnParameters = {
247                @OperationParam(name = "message", typeName = "string", min = 1, max = 1)
248        })
249        public IBaseParameters invalidateValueSetExpansion(
250                @IdParam IIdType theValueSetId,
251                RequestDetails theRequestDetails,
252                HttpServletRequest theServletRequest) {
253                startRequest(theServletRequest);
254                try {
255
256                        String outcome = myTermReadSvc.invalidatePreCalculatedExpansion(theValueSetId, theRequestDetails);
257
258                        IBaseParameters retVal = ParametersUtil.newInstance(getContext());
259                        ParametersUtil.addParameterToParametersString(getContext(), retVal, "message", outcome);
260                        return retVal;
261
262                } finally {
263                        endRequest(theServletRequest);
264                }
265        }
266
267
268        public static ValueSetExpansionOptions createValueSetExpansionOptions(DaoConfig theDaoConfig, IPrimitiveType<Integer> theOffset, IPrimitiveType<Integer> theCount, IPrimitiveType<Boolean> theIncludeHierarchy, IPrimitiveType<String> theFilter) {
269                int offset = theDaoConfig.getPreExpandValueSetsDefaultOffset();
270                if (theOffset != null && theOffset.hasValue()) {
271                        if (theOffset.getValue() >= 0) {
272                                offset = theOffset.getValue();
273                        } else {
274                                throw new InvalidRequestException(Msg.code(1135) + "offset parameter for $expand operation must be >= 0 when specified. offset: " + theOffset.getValue());
275                        }
276                }
277
278                int count = theDaoConfig.getPreExpandValueSetsDefaultCount();
279                if (theCount != null && theCount.hasValue()) {
280                        if (theCount.getValue() >= 0) {
281                                count = theCount.getValue();
282                        } else {
283                                throw new InvalidRequestException(Msg.code(1136) + "count parameter for $expand operation must be >= 0 when specified. count: " + theCount.getValue());
284                        }
285                }
286                int countMax = theDaoConfig.getPreExpandValueSetsMaxCount();
287                if (count > countMax) {
288                        ourLog.warn("count parameter for $expand operation of {} exceeds maximum value of {}; using maximum value.", count, countMax);
289                        count = countMax;
290                }
291
292                ValueSetExpansionOptions options = ValueSetExpansionOptions.forOffsetAndCount(offset, count);
293
294                if (theIncludeHierarchy != null && Boolean.TRUE.equals(theIncludeHierarchy.getValue())) {
295                        options.setIncludeHierarchy(true);
296                }
297
298                if (theFilter != null) {
299                        options.setFilter(theFilter.getValue());
300                }
301
302                return options;
303        }
304
305        private static boolean moreThanOneTrue(boolean... theBooleans) {
306                boolean haveOne = false;
307                for (boolean next : theBooleans) {
308                        if (next) {
309                                if (haveOne) {
310                                        return true;
311                                } else {
312                                        haveOne = true;
313                                }
314                        }
315                }
316                return false;
317        }
318}
319