View Javadoc
1   /*
2    * Copyright 2016 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *       http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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   * Validator for ITI-65 transactions.
38   *
39   * @author Christian Ohr
40   * @since 3.4
41   */
42  public class Iti65Validator extends FhirTransactionValidator.Support {
43  
44      private static final IValidationSupport VALIDATION_SUPPORT = new CustomValidationSupport("profiles/MHD");
45  
46      // Prepare the required validator instances so that the structure definitions are not reloaded each time
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       * Validates bundle type, meta data and consistency of contained resources
80       *
81       * @param bundle transaction bundle
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      * Verifies that bundle has expected content and consistent patient references
106      *
107      * @param bundle transaction bundle
108      */
109     protected void validateBundleConsistency(Bundle bundle) {
110 
111         Map<ResourceType, List<Bundle.BundleEntryComponent>> entries = FhirUtils.getBundleEntries(bundle);
112 
113         // Verify that the bundle has all required resources
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         // Could be contained resources
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 }