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