001package ca.uhn.fhir.jpa.dao;
002
003/*
004 * #%L
005 * HAPI FHIR JPA Server
006 * %%
007 * Copyright (C) 2014 - 2021 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.FhirContext;
024import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
025import ca.uhn.fhir.context.support.IValidationSupport;
026import ca.uhn.fhir.context.support.IValidationSupport.CodeValidationResult;
027import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
028import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem;
029import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet;
030import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
031import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
032import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
033import ca.uhn.fhir.model.dstu2.composite.CodingDt;
034import ca.uhn.fhir.model.dstu2.resource.ValueSet;
035import ca.uhn.fhir.model.dstu2.resource.ValueSet.CodeSystemConcept;
036import ca.uhn.fhir.model.dstu2.resource.ValueSet.ComposeInclude;
037import ca.uhn.fhir.model.dstu2.resource.ValueSet.ComposeIncludeConcept;
038import ca.uhn.fhir.model.dstu2.resource.ValueSet.ExpansionContains;
039import ca.uhn.fhir.model.primitive.DateTimeDt;
040import ca.uhn.fhir.model.primitive.IdDt;
041import ca.uhn.fhir.rest.api.server.IBundleProvider;
042import ca.uhn.fhir.rest.api.server.RequestDetails;
043import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
044import ca.uhn.fhir.rest.param.TokenParam;
045import ca.uhn.fhir.rest.param.UriParam;
046import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
047import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
048import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport;
049import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
050import org.hl7.fhir.instance.model.api.IIdType;
051import org.hl7.fhir.instance.model.api.IPrimitiveType;
052import org.springframework.beans.factory.annotation.Autowired;
053import org.springframework.beans.factory.annotation.Qualifier;
054
055import javax.annotation.Nonnull;
056import javax.annotation.PostConstruct;
057import javax.transaction.Transactional;
058import java.util.ArrayList;
059import java.util.Collections;
060import java.util.List;
061import java.util.Set;
062
063import static ca.uhn.fhir.jpa.dao.dstu3.FhirResourceDaoValueSetDstu3.vsValidateCodeOptions;
064import static ca.uhn.fhir.jpa.util.LogicUtil.multiXor;
065import static org.apache.commons.lang3.StringUtils.isBlank;
066import static org.apache.commons.lang3.StringUtils.isNotBlank;
067
068public class FhirResourceDaoValueSetDstu2 extends BaseHapiFhirResourceDao<ValueSet>
069        implements IFhirResourceDaoValueSet<ValueSet, CodingDt, CodeableConceptDt>, IFhirResourceDaoCodeSystem<ValueSet, CodingDt, CodeableConceptDt> {
070
071        private DefaultProfileValidationSupport myDefaultProfileValidationSupport;
072
073        @Autowired
074        private IValidationSupport myJpaValidationSupport;
075
076        @Autowired
077        @Qualifier("myFhirContextDstu2Hl7Org")
078        private FhirContext myRiCtx;
079        @Autowired
080        private FhirContext myFhirContext;
081
082        private CachingValidationSupport myValidationSupport;
083
084        private void addCompose(String theFilter, ValueSet theValueSetToPopulate, ValueSet theSourceValueSet, CodeSystemConcept theConcept) {
085                if (isBlank(theFilter)) {
086                        addCompose(theValueSetToPopulate, theSourceValueSet.getCodeSystem().getSystem(), theConcept.getCode(), theConcept.getDisplay());
087                } else {
088                        String filter = theFilter.toLowerCase();
089                        if (theConcept.getDisplay().toLowerCase().contains(filter) || theConcept.getCode().toLowerCase().contains(filter)) {
090                                addCompose(theValueSetToPopulate, theSourceValueSet.getCodeSystem().getSystem(), theConcept.getCode(), theConcept.getDisplay());
091                        }
092                }
093                for (CodeSystemConcept nextChild : theConcept.getConcept()) {
094                        addCompose(theFilter, theValueSetToPopulate, theSourceValueSet, nextChild);
095                }
096        }
097
098        private void addCompose(ValueSet retVal, String theSystem, String theCode, String theDisplay) {
099                if (isBlank(theCode)) {
100                        return;
101                }
102                ExpansionContains contains = retVal.getExpansion().addContains();
103                contains.setSystem(theSystem);
104                contains.setCode(theCode);
105                contains.setDisplay(theDisplay);
106        }
107
108        @Override
109        public ValueSet expand(IIdType theId, ValueSetExpansionOptions theOptions, RequestDetails theRequest) {
110                ValueSet source = loadValueSetForExpansion(theId, theRequest);
111                return expand(source, theOptions);
112        }
113
114        @Override
115        public ValueSet expand(ValueSet source, ValueSetExpansionOptions theOptions) {
116                ValueSet retVal = new ValueSet();
117                retVal.setDate(DateTimeDt.withCurrentTime());
118
119
120                String filter = null;
121                if (theOptions != null) {
122                        filter = theOptions.getFilter();
123                }
124
125                /*
126                 * Add composed concepts
127                 */
128
129                for (ComposeInclude nextInclude : source.getCompose().getInclude()) {
130                        for (ComposeIncludeConcept next : nextInclude.getConcept()) {
131                                if (isBlank(filter)) {
132                                        addCompose(retVal, nextInclude.getSystem(), next.getCode(), next.getDisplay());
133                                } else {
134                                        filter = filter.toLowerCase();
135                                        if (next.getDisplay().toLowerCase().contains(filter) || next.getCode().toLowerCase().contains(filter)) {
136                                                addCompose(retVal, nextInclude.getSystem(), next.getCode(), next.getDisplay());
137                                        }
138                                }
139                        }
140                }
141
142                /*
143                 * Add defined concepts
144                 */
145
146                for (CodeSystemConcept next : source.getCodeSystem().getConcept()) {
147                        addCompose(filter, retVal, source, next);
148                }
149
150                return retVal;
151        }
152
153        @Override
154        public ValueSet expandByIdentifier(String theUri, ValueSetExpansionOptions theOptions) {
155                if (isBlank(theUri)) {
156                        throw new InvalidRequestException("URI must not be blank or missing");
157                }
158                ValueSet source;
159
160                ValueSet defaultValueSet = myDefaultProfileValidationSupport.fetchResource(ValueSet.class, theUri);
161                if (defaultValueSet != null) {
162                        source = getContext().newJsonParser().parseResource(ValueSet.class, myRiCtx.newJsonParser().encodeResourceToString(defaultValueSet));
163                } else {
164                        SearchParameterMap params = new SearchParameterMap();
165                        params.setLoadSynchronousUpTo(1);
166                        params.add(ValueSet.SP_URL, new UriParam(theUri));
167                        IBundleProvider ids = search(params);
168                        if (ids.size() == 0) {
169                                throw new InvalidRequestException("Unknown ValueSet URI: " + theUri);
170                        }
171                        source = (ValueSet) ids.getResources(0, 1).get(0);
172                }
173
174                return expand(source, theOptions);
175        }
176
177        @Override
178        public List<IIdType> findCodeSystemIdsContainingSystemAndCode(String theCode, String theSystem, RequestDetails theRequest) {
179                if (theSystem != null && theSystem.startsWith("http://hl7.org/fhir/")) {
180                        return Collections.singletonList(new IdDt(theSystem));
181                }
182
183                List<IIdType> valueSetIds;
184                Set<ResourcePersistentId> ids = searchForIds(new SearchParameterMap(ValueSet.SP_CODE, new TokenParam(theSystem, theCode)), theRequest);
185                valueSetIds = new ArrayList<>();
186                for (ResourcePersistentId next : ids) {
187                        IIdType id = myIdHelperService.translatePidIdToForcedId(myFhirContext, "ValueSet", next);
188                        valueSetIds.add(id);
189                }
190                return valueSetIds;
191        }
192
193        private ValueSet loadValueSetForExpansion(IIdType theId, RequestDetails theRequest) {
194                if (theId.getValue().startsWith("http://hl7.org/fhir/")) {
195                        org.hl7.fhir.dstu2.model.ValueSet valueSet = myValidationSupport.fetchResource(org.hl7.fhir.dstu2.model.ValueSet.class, theId.getValue());
196                        if (valueSet != null) {
197                                return getContext().newJsonParser().parseResource(ValueSet.class, myRiCtx.newJsonParser().encodeResourceToString(valueSet));
198                        }
199                }
200                BaseHasResource sourceEntity = readEntity(theId, theRequest);
201                if (sourceEntity == null) {
202                        throw new ResourceNotFoundException(theId);
203                }
204                ValueSet source = (ValueSet) toResource(sourceEntity, false);
205                return source;
206        }
207
208        private IValidationSupport.LookupCodeResult lookup(List<ExpansionContains> theContains, String theSystem, String theCode) {
209                for (ExpansionContains nextCode : theContains) {
210
211                        String system = nextCode.getSystem();
212                        String code = nextCode.getCode();
213                        if (theSystem.equals(system) && theCode.equals(code)) {
214                                IValidationSupport.LookupCodeResult retVal = new IValidationSupport.LookupCodeResult();
215                                retVal.setSearchedForCode(code);
216                                retVal.setSearchedForSystem(system);
217                                retVal.setFound(true);
218                                if (nextCode.getAbstract() != null) {
219                                        retVal.setCodeIsAbstract(nextCode.getAbstract());
220                                }
221                                retVal.setCodeDisplay(nextCode.getDisplay());
222                                retVal.setCodeSystemVersion(nextCode.getVersion());
223                                retVal.setCodeSystemDisplayName("Unknown"); // TODO: implement
224                                return retVal;
225                        }
226
227                }
228
229                return null;
230        }
231
232        @Nonnull
233        @Override
234        @Transactional
235        public IValidationSupport.LookupCodeResult lookupCode(IPrimitiveType<String> theCode, IPrimitiveType<String> theSystem, CodingDt theCoding,  RequestDetails theRequest) {
236                return lookupCode(theCode, theSystem, theCoding, null, theRequest);
237        }
238        
239        @Nonnull
240        @Override
241        @Transactional
242        public IValidationSupport.LookupCodeResult lookupCode(IPrimitiveType<String> theCode, IPrimitiveType<String> theSystem, CodingDt theCoding, IPrimitiveType<String> theDisplayLanguage, RequestDetails theRequest) {
243                boolean haveCoding = theCoding != null && isNotBlank(theCoding.getSystem()) && isNotBlank(theCoding.getCode());
244                boolean haveCode = theCode != null && theCode.isEmpty() == false;
245                boolean haveSystem = theSystem != null && theSystem.isEmpty() == false;
246
247                if (!haveCoding && !(haveSystem && haveCode)) {
248                        throw new InvalidRequestException("No code, coding, or codeableConcept provided to validate");
249                }
250                if (!multiXor(haveCoding, (haveSystem && haveCode)) || (haveSystem != haveCode)) {
251                        throw new InvalidRequestException("$lookup can only validate (system AND code) OR (coding.system AND coding.code)");
252                }
253
254                String code;
255                String system;
256                if (haveCoding) {
257                        code = theCoding.getCode();
258                        system = theCoding.getSystem();
259                } else {
260                        code = theCode.getValue();
261                        system = theSystem.getValue();
262                }
263
264                List<IIdType> valueSetIds = findCodeSystemIdsContainingSystemAndCode(code, system, theRequest);
265                for (IIdType nextId : valueSetIds) {
266                        ValueSet expansion = expand(nextId, null, theRequest);
267                        List<ExpansionContains> contains = expansion.getExpansion().getContains();
268                        IValidationSupport.LookupCodeResult result = lookup(contains, system, code);
269                        if (result != null) {
270                                return result;
271                        }
272                }
273
274                IValidationSupport.LookupCodeResult retVal = new IValidationSupport.LookupCodeResult();
275                retVal.setFound(false);
276                retVal.setSearchedForCode(code);
277                retVal.setSearchedForSystem(system);
278                return retVal;
279        }
280
281        @Override
282        public SubsumesResult subsumes(IPrimitiveType<String> theCodeA, IPrimitiveType<String> theCodeB, IPrimitiveType<String> theSystem, CodingDt theCodingA, CodingDt theCodingB, RequestDetails theRequestDetails) {
283                return myTerminologySvc.subsumes(theCodeA, theCodeB, theSystem, theCodingA, theCodingB);
284        }
285
286        @Override
287        @PostConstruct
288        public void postConstruct() {
289                super.postConstruct();
290                myDefaultProfileValidationSupport = new DefaultProfileValidationSupport(myFhirContext);
291                myValidationSupport = new CachingValidationSupport(new ValidationSupportChain(myDefaultProfileValidationSupport, myJpaValidationSupport));
292        }
293
294        @Override
295        public void purgeCaches() {
296                // nothing
297        }
298
299        @Override
300        public IValidationSupport.CodeValidationResult validateCode(IPrimitiveType<String> theValueSetIdentifier, IIdType theId, IPrimitiveType<String> theCode,
301                                                                                                                                                                        IPrimitiveType<String> theSystem, IPrimitiveType<String> theDisplay, CodingDt theCoding, CodeableConceptDt theCodeableConcept, RequestDetails theRequest) {
302                return myTerminologySvc.validateCode(vsValidateCodeOptions(), theId, toStringOrNull(theValueSetIdentifier), toStringOrNull(theSystem), toStringOrNull(theCode), toStringOrNull(theDisplay), theCoding, theCodeableConcept);
303        }
304
305        @Override
306        public CodeValidationResult validateCode(IIdType theCodeSystemId, IPrimitiveType<String> theCodeSystemUrl, IPrimitiveType<String> theVersion, IPrimitiveType<String> theCode,
307                                                                                                                  IPrimitiveType<String> theDisplay, CodingDt theCoding, CodeableConceptDt theCodeableConcept, RequestDetails theRequestDetails) {
308                throw new UnsupportedOperationException();
309        }
310
311        public static String toStringOrNull(IPrimitiveType<String> thePrimitive) {
312                return thePrimitive != null ? thePrimitive.getValue() : null;
313        }
314
315}