1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.openehealth.ipf.commons.ihe.fhir.iti65;
18
19 import ca.uhn.fhir.context.FhirContext;
20 import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
21 import ca.uhn.fhir.validation.FhirValidator;
22 import ca.uhn.fhir.validation.ValidationResult;
23 import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
24 import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator;
25 import org.hl7.fhir.dstu3.model.*;
26 import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
27 import org.hl7.fhir.instance.model.api.IBaseResource;
28 import org.openehealth.ipf.commons.ihe.fhir.FhirTransactionValidator;
29 import org.openehealth.ipf.commons.ihe.fhir.support.CustomValidationSupport;
30 import org.openehealth.ipf.commons.ihe.fhir.support.FhirUtils;
31 import org.openehealth.ipf.commons.ihe.xds.core.responses.ErrorCode;
32
33 import java.util.*;
34 import java.util.function.Function;
35
36
37
38
39
40
41
42 public class Iti65Validator extends FhirTransactionValidator.Support {
43
44 private static final IValidationSupport VALIDATION_SUPPORT = new CustomValidationSupport("profiles/MHD");
45
46
47 private static Map<Class<?>, FhirInstanceValidator> VALIDATORS = new HashMap<>();
48
49
50 static {
51 VALIDATORS.put(DocumentManifest.class, new FhirInstanceValidator(VALIDATION_SUPPORT));
52 VALIDATORS.put(DocumentReference.class, new FhirInstanceValidator(VALIDATION_SUPPORT));
53 VALIDATORS.put(ListResource.class, new FhirInstanceValidator(VALIDATION_SUPPORT));
54 }
55
56
57 @Override
58 public void validateRequest(FhirContext context, Object payload, Map<String, Object> parameters) {
59 Bundle transactionBundle = (Bundle) payload;
60 validateTransactionBundle(transactionBundle);
61 validateBundleConsistency(transactionBundle);
62
63 for (Bundle.BundleEntryComponent entry : transactionBundle.getEntry()) {
64 Class<? extends IBaseResource> clazz = entry.getResource().getClass();
65 if (VALIDATORS.containsKey(clazz)) {
66 FhirValidator validator = context.newValidator();
67 validator.registerValidatorModule(VALIDATORS.get(clazz));
68 ValidationResult validationResult = validator.validateWithResult(entry.getResource());
69 if (!validationResult.isSuccessful()) {
70 IBaseOperationOutcome operationOutcome = validationResult.toOperationOutcome();
71 throw FhirUtils.exception(UnprocessableEntityException::new, operationOutcome, "Validation Failed");
72 }
73 }
74 }
75 }
76
77
78
79
80
81
82
83 protected void validateTransactionBundle(Bundle bundle) {
84 if (!Bundle.BundleType.TRANSACTION.equals(bundle.getType())) {
85 throw FhirUtils.unprocessableEntity(
86 OperationOutcome.IssueSeverity.ERROR,
87 OperationOutcome.IssueType.INVALID,
88 null, null,
89 "Bundle type must be %s, but was %s",
90 Bundle.BundleType.TRANSACTION.toCode(), bundle.getType().toCode());
91 }
92 List<UriType> profiles = bundle.getMeta().getProfile();
93 if (profiles.isEmpty() || !Iti65Constants.ITI65_PROFILE.equals(profiles.get(0).getValue())) {
94 throw FhirUtils.unprocessableEntity(
95 OperationOutcome.IssueSeverity.ERROR,
96 OperationOutcome.IssueType.INVALID,
97 null, null,
98 "Request bundle must have profile",
99 Iti65Constants.ITI65_PROFILE);
100 }
101
102 }
103
104
105
106
107
108
109 protected void validateBundleConsistency(Bundle bundle) {
110
111 Map<ResourceType, List<Bundle.BundleEntryComponent>> entries = FhirUtils.getBundleEntries(bundle);
112
113
114 if (entries.getOrDefault(ResourceType.DocumentManifest, Collections.emptyList()).size() != 1) {
115 throw FhirUtils.unprocessableEntity(
116 OperationOutcome.IssueSeverity.ERROR,
117 OperationOutcome.IssueType.INVALID,
118 null, null,
119 "Request bundle must have exactly one DocumentManifest"
120 );
121 }
122 if (entries.getOrDefault(ResourceType.DocumentReference, Collections.emptyList()).isEmpty()) {
123 throw FhirUtils.unprocessableEntity(
124 OperationOutcome.IssueSeverity.ERROR,
125 OperationOutcome.IssueType.INVALID,
126 null, null,
127 "Request bundle must have at least one DocumentReference"
128 );
129 }
130
131 Set<String> patientReferences = new HashSet<>();
132 Set<String> expectedBinaryFullUrls = new HashSet<>();
133 Set<String> expectedReferenceFullUrls = new HashSet<>();
134 entries.values().stream()
135 .flatMap(Collection::stream)
136 .map(Bundle.BundleEntryComponent::getResource)
137 .forEach(resource -> {
138 if (resource instanceof DocumentManifest) {
139 DocumentManifest dm = (DocumentManifest) resource;
140 for (DocumentManifest.DocumentManifestContentComponent content : dm.getContent()) {
141 try {
142 expectedReferenceFullUrls.add(content.getPReference().getReference());
143 } catch (Exception ignored) {
144 }
145 }
146 patientReferences.add(getSubjectReference(resource, r -> dm.getSubject()));
147 } else if (resource instanceof DocumentReference) {
148 DocumentReference dr = (DocumentReference) resource;
149 for (DocumentReference.DocumentReferenceContentComponent content : dr.getContent()) {
150 expectedBinaryFullUrls.add(content.getAttachment().getUrl());
151 }
152 patientReferences.add(getSubjectReference(resource, r -> ((DocumentReference) r).getSubject()));
153 } else if (resource instanceof ListResource) {
154 patientReferences.add(getSubjectReference(resource, r -> ((ListResource) r).getSubject()));
155 } else if (!(resource instanceof Binary)) {
156 throw FhirUtils.unprocessableEntity(
157 OperationOutcome.IssueSeverity.ERROR,
158 OperationOutcome.IssueType.INVALID,
159 null, null,
160 "Unexpected bundle component %s",
161 resource.getClass().getSimpleName()
162 );
163 }
164 });
165
166 if (patientReferences.size() != 1) {
167 throw FhirUtils.unprocessableEntity(
168 OperationOutcome.IssueSeverity.ERROR,
169 OperationOutcome.IssueType.INVALID,
170 ErrorCode.PATIENT_ID_DOES_NOT_MATCH.getOpcode(),
171 null,
172 "Inconsistent patient references %s",
173 patientReferences
174 );
175 }
176
177 entries.values().stream()
178 .flatMap(Collection::stream)
179 .forEach(entry -> {
180 if (ResourceType.DocumentReference == entry.getResource().getResourceType()) {
181 if (!expectedReferenceFullUrls.remove(entry.getFullUrl())) {
182 throw FhirUtils.unprocessableEntity(
183 OperationOutcome.IssueSeverity.ERROR,
184 OperationOutcome.IssueType.INVALID,
185 null, null,
186 "DocumentReference with URL %s is not referenced by any DocumentManifest",
187 entry.getFullUrl()
188 );
189 }
190 } else if (ResourceType.Binary == entry.getResource().getResourceType()) {
191 if (!expectedBinaryFullUrls.remove(entry.getFullUrl())) {
192 throw FhirUtils.unprocessableEntity(
193 OperationOutcome.IssueSeverity.ERROR,
194 OperationOutcome.IssueType.INVALID,
195 null, null,
196 "Binary with URL %s is not referenced by any DocumentReference",
197 entry.getFullUrl()
198 );
199 }
200 }
201 });
202
203 if (!expectedBinaryFullUrls.isEmpty()) {
204 throw FhirUtils.unprocessableEntity(
205 OperationOutcome.IssueSeverity.ERROR,
206 OperationOutcome.IssueType.INVALID,
207 null, null,
208 "Binary with URLs %s referenced, but not present in this bundle",
209 expectedBinaryFullUrls
210 );
211 }
212
213 if (!expectedReferenceFullUrls.isEmpty()) {
214 throw FhirUtils.unprocessableEntity(
215 OperationOutcome.IssueSeverity.ERROR,
216 OperationOutcome.IssueType.INVALID,
217 null, null,
218 "DocumentReference with URLs %s referenced, but not present in this bundle",
219 expectedBinaryFullUrls
220 );
221 }
222
223 }
224
225
226 private String getSubjectReference(Resource resource, Function<Resource, Reference> f) {
227 Reference reference = f.apply(resource);
228 if (reference == null) {
229 throw FhirUtils.unprocessableEntity(
230 OperationOutcome.IssueSeverity.ERROR,
231 OperationOutcome.IssueType.INVALID,
232 ErrorCode.UNKNOWN_PATIENT_ID.getOpcode(),
233 null,
234 "Empty Patient reference in resource %s",
235 resource
236 );
237 }
238
239 if (reference.getResource() != null) {
240 Patient patient = (Patient) reference.getResource();
241 return patient.getIdentifier().get(0).getValue();
242 }
243 return reference.getReference();
244 }
245
246
247 }