View Javadoc
1   package ca.uhn.fhir.jpa.dao;
2   
3   /*
4    * #%L
5    * HAPI FHIR JPA Server
6    * %%
7    * Copyright (C) 2014 - 2018 University Health Network
8    * %%
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   * 
13   * http://www.apache.org/licenses/LICENSE-2.0
14   * 
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * #L%
21   */
22  
23  import static org.apache.commons.lang3.StringUtils.isBlank;
24  import static org.apache.commons.lang3.StringUtils.isNotBlank;
25  
26  import java.util.*;
27  
28  import javax.annotation.PostConstruct;
29  
30  import org.apache.commons.codec.binary.StringUtils;
31  import org.hl7.fhir.instance.hapi.validation.CachingValidationSupport;
32  import org.hl7.fhir.instance.hapi.validation.DefaultProfileValidationSupport;
33  import org.hl7.fhir.instance.hapi.validation.ValidationSupportChain;
34  import org.hl7.fhir.instance.model.api.IIdType;
35  import org.hl7.fhir.instance.model.api.IPrimitiveType;
36  import org.springframework.beans.factory.annotation.Autowired;
37  import org.springframework.beans.factory.annotation.Qualifier;
38  
39  import ca.uhn.fhir.context.FhirContext;
40  import ca.uhn.fhir.jpa.entity.BaseHasResource;
41  import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt;
42  import ca.uhn.fhir.model.dstu2.composite.CodingDt;
43  import ca.uhn.fhir.model.dstu2.resource.ValueSet;
44  import ca.uhn.fhir.model.dstu2.resource.ValueSet.*;
45  import ca.uhn.fhir.model.primitive.DateTimeDt;
46  import ca.uhn.fhir.model.primitive.IdDt;
47  import ca.uhn.fhir.rest.api.server.IBundleProvider;
48  import ca.uhn.fhir.rest.api.server.RequestDetails;
49  import ca.uhn.fhir.rest.param.TokenParam;
50  import ca.uhn.fhir.rest.param.UriParam;
51  import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
52  import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
53  
54  public class FhirResourceDaoValueSetDstu2 extends FhirResourceDaoDstu2<ValueSet>
55  		implements IFhirResourceDaoValueSet<ValueSet, CodingDt, CodeableConceptDt>, IFhirResourceDaoCodeSystem<ValueSet, CodingDt, CodeableConceptDt> {
56  
57  	private DefaultProfileValidationSupport myDefaultProfileValidationSupport;
58  
59  	@Autowired
60  	private IJpaValidationSupportDstu2 myJpaValidationSupport;
61  
62  	@Autowired
63  	@Qualifier("myFhirContextDstu2Hl7Org")
64  	private FhirContext myRiCtx;
65  
66  	private CachingValidationSupport myValidationSupport;
67  
68  	private void addCompose(String theFilter, ValueSet theValueSetToPopulate, ValueSet theSourceValueSet, CodeSystemConcept theConcept) {
69  		if (isBlank(theFilter)) {
70  			addCompose(theValueSetToPopulate, theSourceValueSet.getCodeSystem().getSystem(), theConcept.getCode(), theConcept.getDisplay());
71  		} else {
72  			String filter = theFilter.toLowerCase();
73  			if (theConcept.getDisplay().toLowerCase().contains(filter) || theConcept.getCode().toLowerCase().contains(filter)) {
74  				addCompose(theValueSetToPopulate, theSourceValueSet.getCodeSystem().getSystem(), theConcept.getCode(), theConcept.getDisplay());
75  			}
76  		}
77  		for (CodeSystemConcept nextChild : theConcept.getConcept()) {
78  			addCompose(theFilter, theValueSetToPopulate, theSourceValueSet, nextChild);
79  		}
80  	}
81  
82  	private void addCompose(ValueSet retVal, String theSystem, String theCode, String theDisplay) {
83  		if (isBlank(theCode)) {
84  			return;
85  		}
86  		ExpansionContains contains = retVal.getExpansion().addContains();
87  		contains.setSystem(theSystem);
88  		contains.setCode(theCode);
89  		contains.setDisplay(theDisplay);
90  	}
91  
92  	@Override
93  	public ValueSet expand(IIdType theId, String theFilter, RequestDetails theRequestDetails) {
94  		ValueSet source = loadValueSetForExpansion(theId);
95  		return expand(source, theFilter);
96  
97  	}
98  
99  	@Override
100 	public ValueSet expand(ValueSet source, String theFilter) {
101 		ValueSet retVal = new ValueSet();
102 		retVal.setDate(DateTimeDt.withCurrentTime());
103 
104 		/*
105 		 * Add composed concepts
106 		 */
107 
108 		for (ComposeInclude nextInclude : source.getCompose().getInclude()) {
109 			for (ComposeIncludeConcept next : nextInclude.getConcept()) {
110 				if (isBlank(theFilter)) {
111 					addCompose(retVal, nextInclude.getSystem(), next.getCode(), next.getDisplay());
112 				} else {
113 					String filter = theFilter.toLowerCase();
114 					if (next.getDisplay().toLowerCase().contains(filter) || next.getCode().toLowerCase().contains(filter)) {
115 						addCompose(retVal, nextInclude.getSystem(), next.getCode(), next.getDisplay());
116 					}
117 				}
118 			}
119 		}
120 
121 		/*
122 		 * Add defined concepts
123 		 */
124 
125 		for (CodeSystemConcept next : source.getCodeSystem().getConcept()) {
126 			addCompose(theFilter, retVal, source, next);
127 		}
128 
129 		return retVal;
130 	}
131 
132 	@Override
133 	public ValueSet expandByIdentifier(String theUri, String theFilter) {
134 		if (isBlank(theUri)) {
135 			throw new InvalidRequestException("URI must not be blank or missing");
136 		}
137 		ValueSet source;
138 
139 		org.hl7.fhir.instance.model.ValueSet defaultValueSet = myDefaultProfileValidationSupport.fetchResource(myRiCtx, org.hl7.fhir.instance.model.ValueSet.class, theUri);
140 		if (defaultValueSet != null) {
141 			source = getContext().newJsonParser().parseResource(ValueSet.class, myRiCtx.newJsonParser().encodeResourceToString(defaultValueSet));
142 		} else {
143 			SearchParameterMap params = new SearchParameterMap();
144 			params.setLoadSynchronousUpTo(1);
145 			params.add(ValueSet.SP_URL, new UriParam(theUri));
146 			IBundleProvider ids = search(params);
147 			if (ids.size() == 0) {
148 				throw new InvalidRequestException("Unknown ValueSet URI: " + theUri);
149 			}
150 			source = (ValueSet) ids.getResources(0, 1).get(0);
151 		}
152 
153 		return expand(source, theFilter);
154 
155 	}
156 
157 	@Override
158 	public List<IIdType> findCodeSystemIdsContainingSystemAndCode(String theCode, String theSystem) {
159 		if (theSystem != null && theSystem.startsWith("http://hl7.org/fhir/")) {
160 			return Collections.singletonList((IIdType) new IdDt(theSystem));
161 		}
162 
163 		List<IIdType> valueSetIds;
164 		Set<Long> ids = searchForIds(new SearchParameterMap(ValueSet.SP_CODE, new TokenParam(theSystem, theCode)));
165 		valueSetIds = new ArrayList<IIdType>();
166 		for (Long next : ids) {
167 			valueSetIds.add(new IdDt("ValueSet", next));
168 		}
169 		return valueSetIds;
170 	}
171 
172 	private ValueSet loadValueSetForExpansion(IIdType theId) {
173 		if (theId.getValue().startsWith("http://hl7.org/fhir/")) {
174 			org.hl7.fhir.instance.model.ValueSet valueSet = myValidationSupport.fetchResource(myRiCtx, org.hl7.fhir.instance.model.ValueSet.class, theId.getValue());
175 			if (valueSet != null) {
176 				return getContext().newJsonParser().parseResource(ValueSet.class, myRiCtx.newJsonParser().encodeResourceToString(valueSet));
177 			}
178 		}
179 		BaseHasResource sourceEntity = readEntity(theId);
180 		if (sourceEntity == null) {
181 			throw new ResourceNotFoundException(theId);
182 		}
183 		ValueSet source = (ValueSet) toResource(sourceEntity, false);
184 		return source;
185 	}
186 
187 	private LookupCodeResult lookup(List<ExpansionContains> theContains, String theSystem, String theCode) {
188 		for (ExpansionContains nextCode : theContains) {
189 
190 			String system = nextCode.getSystem();
191 			String code = nextCode.getCode();
192 			if (theSystem.equals(system) && theCode.equals(code)) {
193 				LookupCodeResult retVal = new LookupCodeResult();
194 				retVal.setSearchedForCode(code);
195 				retVal.setSearchedForSystem(system);
196 				retVal.setFound(true);
197 				if (nextCode.getAbstract() != null) {
198 					retVal.setCodeIsAbstract(nextCode.getAbstract());
199 				}
200 				retVal.setCodeDisplay(nextCode.getDisplay());
201 				retVal.setCodeSystemVersion(nextCode.getVersion());
202 				retVal.setCodeSystemDisplayName("Unknown"); // TODO: implement
203 				return retVal;
204 			}
205 
206 		}
207 
208 		return null;
209 	}
210 
211 	@Override
212 	public LookupCodeResult lookupCode(IPrimitiveType<String> theCode, IPrimitiveType<String> theSystem, CodingDt theCoding, RequestDetails theRequestDetails) {
213 		boolean haveCoding = theCoding != null && isNotBlank(theCoding.getSystem()) && isNotBlank(theCoding.getCode());
214 		boolean haveCode = theCode != null && theCode.isEmpty() == false;
215 		boolean haveSystem = theSystem != null && theSystem.isEmpty() == false;
216 
217 		if (!haveCoding && !(haveSystem && haveCode)) {
218 			throw new InvalidRequestException("No code, coding, or codeableConcept provided to validate");
219 		}
220 		if (!multiXor(haveCoding, (haveSystem && haveCode)) || (haveSystem != haveCode)) {
221 			throw new InvalidRequestException("$lookup can only validate (system AND code) OR (coding.system AND coding.code)");
222 		}
223 
224 		String code;
225 		String system;
226 		if (haveCoding) {
227 			code = theCoding.getCode();
228 			system = theCoding.getSystem();
229 		} else {
230 			code = theCode.getValue();
231 			system = theSystem.getValue();
232 		}
233 
234 		List<IIdType> valueSetIds = findCodeSystemIdsContainingSystemAndCode(code, system);
235 		for (IIdType nextId : valueSetIds) {
236 			ValueSet expansion = expand(nextId, null, theRequestDetails);
237 			List<ExpansionContains> contains = expansion.getExpansion().getContains();
238 			LookupCodeResult result = lookup(contains, system, code);
239 			if (result != null) {
240 				return result;
241 			}
242 		}
243 
244 		LookupCodeResult retVal = new LookupCodeResult();
245 		retVal.setFound(false);
246 		retVal.setSearchedForCode(code);
247 		retVal.setSearchedForSystem(system);
248 		return retVal;
249 	}
250 
251 	@Override
252 	@PostConstruct
253 	public void postConstruct() {
254 		super.postConstruct();
255 		myDefaultProfileValidationSupport = new DefaultProfileValidationSupport();
256 		myValidationSupport = new CachingValidationSupport(new ValidationSupportChain(myDefaultProfileValidationSupport, myJpaValidationSupport));
257 	}
258 
259 	@Override
260 	public void purgeCaches() {
261 		// nothing
262 	}
263 
264 	private String toStringOrNull(IPrimitiveType<String> thePrimitive) {
265 		return thePrimitive != null ? thePrimitive.getValue() : null;
266 	}
267 
268 	@Override
269 	public ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.ValidateCodeResult validateCode(IPrimitiveType<String> theValueSetIdentifier, IIdType theId, IPrimitiveType<String> theCode,
270 			IPrimitiveType<String> theSystem, IPrimitiveType<String> theDisplay, CodingDt theCoding, CodeableConceptDt theCodeableConcept, RequestDetails theRequestDetails) {
271 		List<IIdType> valueSetIds;
272 
273 		boolean haveCodeableConcept = theCodeableConcept != null && theCodeableConcept.getCoding().size() > 0;
274 		boolean haveCoding = theCoding != null && theCoding.isEmpty() == false;
275 		boolean haveCode = theCode != null && theCode.isEmpty() == false;
276 
277 		if (!haveCodeableConcept && !haveCoding && !haveCode) {
278 			throw new InvalidRequestException("No code, coding, or codeableConcept provided to validate");
279 		}
280 		if (!multiXor(haveCodeableConcept, haveCoding, haveCode)) {
281 			throw new InvalidRequestException("$validate-code can only validate (system AND code) OR (coding) OR (codeableConcept)");
282 		}
283 
284 		boolean haveIdentifierParam = theValueSetIdentifier != null && theValueSetIdentifier.isEmpty() == false;
285 		if (theId != null) {
286 			valueSetIds = Collections.singletonList(theId);
287 		} else if (haveIdentifierParam) {
288 			Set<Long> ids = searchForIds(new SearchParameterMap(ValueSet.SP_IDENTIFIER, new TokenParam(null, theValueSetIdentifier.getValue())));
289 			valueSetIds = new ArrayList<IIdType>();
290 			for (Long next : ids) {
291 				valueSetIds.add(new IdDt("ValueSet", next));
292 			}
293 		} else {
294 			if (theCode == null || theCode.isEmpty()) {
295 				throw new InvalidRequestException("Either ValueSet ID or ValueSet identifier or system and code must be provided. Unable to validate.");
296 			}
297 			String code = theCode.getValue();
298 			String system = toStringOrNull(theSystem);
299 			valueSetIds = findCodeSystemIdsContainingSystemAndCode(code, system);
300 		}
301 
302 		for (IIdType nextId : valueSetIds) {
303 			ValueSet expansion = expand(nextId, null, theRequestDetails);
304 			List<ExpansionContains> contains = expansion.getExpansion().getContains();
305 			ValidateCodeResult result = validateCodeIsInContains(contains, toStringOrNull(theSystem), toStringOrNull(theCode), theCoding, theCodeableConcept);
306 			if (result != null) {
307 				if (theDisplay != null && isNotBlank(theDisplay.getValue()) && isNotBlank(result.getDisplay())) {
308 					if (!theDisplay.getValue().equals(result.getDisplay())) {
309 						return new ValidateCodeResult(false, "Display for code does not match", result.getDisplay());
310 					}
311 				}
312 				return result;
313 			}
314 		}
315 
316 		return new ValidateCodeResult(false, "Code not found", null);
317 	}
318 
319 	private ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.ValidateCodeResult validateCodeIsInContains(List<ExpansionContains> contains, String theSystem, String theCode, CodingDt theCoding,
320 			CodeableConceptDt theCodeableConcept) {
321 		for (ExpansionContains nextCode : contains) {
322 			ca.uhn.fhir.jpa.dao.IFhirResourceDaoValueSet.ValidateCodeResult result = validateCodeIsInContains(nextCode.getContains(), theSystem, theCode, theCoding, theCodeableConcept);
323 			if (result != null) {
324 				return result;
325 			}
326 
327 			String system = nextCode.getSystem();
328 			String code = nextCode.getCode();
329 
330 			if (isNotBlank(theCode)) {
331 				if (theCode.equals(code) && (isBlank(theSystem) || theSystem.equals(system))) {
332 					return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay());
333 				}
334 			} else if (theCoding != null) {
335 				if (StringUtils.equals(system, theCoding.getSystem()) && StringUtils.equals(code, theCoding.getCode())) {
336 					return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay());
337 				}
338 			} else {
339 				for (CodingDt next : theCodeableConcept.getCoding()) {
340 					if (StringUtils.equals(system, next.getSystem()) && StringUtils.equals(code, next.getCode())) {
341 						return new ValidateCodeResult(true, "Validation succeeded", nextCode.getDisplay());
342 					}
343 				}
344 			}
345 
346 		}
347 
348 		return null;
349 	}
350 
351 	private static boolean multiXor(boolean... theValues) {
352 		int count = 0;
353 		for (int i = 0; i < theValues.length; i++) {
354 			if (theValues[i]) {
355 				count++;
356 			}
357 		}
358 		return count == 1;
359 	}
360 
361 }