Skip to content

Commit

Permalink
Merge branch 'feature/iti-105'
Browse files Browse the repository at this point in the history
# Conflicts:
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/iti66/Iti66ListSearchParameters.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/ComprehensiveDocumentReference.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/ComprehensiveFolderList.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/ComprehensiveProvideDocumentBundle.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/ComprehensiveSubmissionSetList.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/FindComprehensiveDocumentReferencesResponseBundle.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/FindDocumentListsResponseBundle.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/FindMinimalDocumentReferencesResponseBundle.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/FolderList.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/MhdList.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/MinimalDocumentReference.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/MinimalFolderList.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/MinimalProvideDocumentBundle.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/MinimalSubmissionSetList.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/ProvideDocumentBundleResponse.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/SimplifiedPublishDocumentReference.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/Source.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/SubmissionSetList.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/UncontainedComprehensiveDocumentReference.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/UncontainedComprehensiveProvideDocumentBundle.java
#	commons/ihe/fhir/r4/mhd/src/main/java/org/openehealth/ipf/commons/ihe/fhir/mhd/model/UncontainedComprehensiveSubmissionSetList.java
#	platform-camel/ihe/fhir/r4/mhd/src/test/java/org/openehealth/ipf/platform/camel/ihe/fhir/iti65/v421/AbstractTestIti65.java
#	platform-camel/ihe/fhir/r4/mhd/src/test/java/org/openehealth/ipf/platform/camel/ihe/fhir/iti66/v421/AbstractTestIti66.java
  • Loading branch information
Christian Ohr committed Dec 20, 2023
2 parents 2e99e10 + 347fa6b commit c706b43
Show file tree
Hide file tree
Showing 85 changed files with 67,965 additions and 183 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import org.openehealth.ipf.commons.ihe.fhir.BundleProfileSelector;

import static org.openehealth.ipf.commons.ihe.fhir.iti65.Iti65Constants.*;
import static org.openehealth.ipf.commons.ihe.fhir.mhd.MhdProfiles.*;
import static org.openehealth.ipf.commons.ihe.fhir.mhd.MhdProfile.*;

/**
* Standard Configuration for Iti65Component.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
import org.hl7.fhir.common.hapi.validation.support.CommonCodeSystemsTerminologyService;
import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.PrePopulatedValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.SnapshotGeneratingValidationSupport;
import org.hl7.fhir.common.hapi.validation.support.ValidationSupportChain;
import org.hl7.fhir.common.hapi.validation.support.*;
import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator;
import org.hl7.fhir.r4.model.Binary;
import org.hl7.fhir.r4.model.Bundle;
Expand All @@ -41,6 +37,7 @@
import org.hl7.fhir.r4.model.StructureDefinition;
import org.hl7.fhir.r5.utils.validation.constants.BestPracticeWarningLevel;
import org.openehealth.ipf.commons.ihe.fhir.FhirTransactionValidator;
import org.openehealth.ipf.commons.ihe.fhir.mhd.Mhd421;
import org.openehealth.ipf.commons.ihe.fhir.support.FhirUtils;
import org.openehealth.ipf.commons.ihe.xds.core.responses.ErrorCode;
import org.slf4j.Logger;
Expand All @@ -54,6 +51,9 @@
import java.util.Optional;
import java.util.Scanner;
import java.util.function.Function;
import java.util.stream.Collectors;

import static org.openehealth.ipf.commons.ihe.fhir.mhd.MhdProfile.*;

/**
* Validator for ITI-65 transactions.
Expand All @@ -69,27 +69,31 @@ public class Iti65Validator extends FhirTransactionValidator.Support {
private static final String IHE_PROFILE_PREFIX = "http://ihe.net/fhir/StructureDefinition/";

private final FhirContext fhirContext;
private IValidationSupport validationSupport;
private IValidationSupport validationSupportv320;

public Iti65Validator(FhirContext fhirContext) {
this.fhirContext = fhirContext;
LOG.info("Initializing Validator for ITI-65 bundles");
validationSupport = loadStructureDefinitions(new DefaultProfileValidationSupport(fhirContext), "Minimal");
validationSupport = loadStructureDefinitions(validationSupport, "Comprehensive");
validationSupportv320 = loadStructureDefinitionsv320(new DefaultProfileValidationSupport(fhirContext), "Minimal");
validationSupportv320 = loadStructureDefinitionsv320(validationSupportv320, "Comprehensive");
validationSupportv320 = new CachingValidationSupport(validationSupportv320);
LOG.info("Initialized Validator for ITI-65 bundles");
}

@Override
public void validateRequest(Object payload, Map<String, Object> parameters) {

var transactionBundle = (Bundle) payload;

validateBundleConsistency(transactionBundle);
if (transactionBundle instanceof Mhd421) {
validateBundleConsistency421(transactionBundle);
} else {
validateBundleConsistency320(transactionBundle);
}

var validator = fhirContext.newValidator();
validator.setValidateAgainstStandardSchema(false);
validator.setValidateAgainstStandardSchematron(false);
var instanceValidator = new FhirInstanceValidator(validationSupport);
var instanceValidator = new FhirInstanceValidator(validationSupportv320);
instanceValidator.setNoTerminologyChecks(false);
instanceValidator.setErrorForUnknownProfiles(true);
instanceValidator.setBestPracticeWarningLevel(BestPracticeWarningLevel.Hint);
Expand All @@ -101,7 +105,7 @@ public void validateRequest(Object payload, Map<String, Object> parameters) {
}
}

public ValidationSupportChain loadStructureDefinitions(IValidationSupport baseValidationSupport, String kind) {
public ValidationSupportChain loadStructureDefinitionsv320(IValidationSupport baseValidationSupport, String kind) {
var validationSupport = new PrePopulatedValidationSupport(fhirContext);
var supportChain = new ValidationSupportChain(
validationSupport,
Expand Down Expand Up @@ -132,8 +136,9 @@ private Optional<StructureDefinition> findProfile(
var profileText = scanner.useDelimiter("\\A").next();
var parser = EncodingEnum.detectEncodingNoDefault(profileText).newParser(fhirContext);
var structureDefinition = parser.parseResource(StructureDefinition.class, profileText);
return Optional.of(structureDefinition.hasSnapshot() ? structureDefinition
: (StructureDefinition) new SnapshotGeneratingValidationSupport(fhirContext).generateSnapshot(
return Optional.of(structureDefinition.hasSnapshot() ?
structureDefinition :
(StructureDefinition) new SnapshotGeneratingValidationSupport(fhirContext).generateSnapshot(
new ValidationSupportContext(snaphotGenerationSupport), structureDefinition, url, url,
name));
}
Expand All @@ -147,7 +152,7 @@ private Optional<StructureDefinition> findProfile(
*
* @param bundle transaction bundle
*/
protected void validateBundleConsistency(Bundle bundle) {
protected void validateBundleConsistency320(Bundle bundle) {

var entries = FhirUtils.getBundleEntries(bundle);

Expand Down Expand Up @@ -269,6 +274,144 @@ protected void validateBundleConsistency(Bundle bundle) {

}

/**
* Verifies that bundle has expected content and consistent patient references
*
* @param bundle transaction bundle
*/
protected void validateBundleConsistency421(Bundle bundle) {

var entries = FhirUtils.getBundleEntries(bundle);

// Verify that the bundle has all required resources
// This should be done by the StructureDefinition, but apparently HAPI has a problem with slices...
// TODO check

var submissionSets = entries.getOrDefault(ResourceType.List, Collections.emptyList()).stream()
.map(Bundle.BundleEntryComponent::getResource)
.map(ListResource.class::cast)
.filter(this::isListSubmissionSet)
.collect(Collectors.toList());
if (submissionSets.size() != 1) {
throw FhirUtils.unprocessableEntity(
OperationOutcome.IssueSeverity.ERROR,
OperationOutcome.IssueType.INVALID,
null, null,
"Request bundle must have exactly one SubmissionSet list"
);
}
if (entries.getOrDefault(ResourceType.DocumentReference, Collections.emptyList()).isEmpty()) {
throw FhirUtils.unprocessableEntity(
OperationOutcome.IssueSeverity.ERROR,
OperationOutcome.IssueType.INVALID,
null, null,
"Request bundle must have at least one DocumentReference"
);
}


var patientReferences = new HashSet<String>();
var expectedBinaryFullUrls = new HashSet<String>();
var expectedReferenceFullUrls = new HashSet<String>();
entries.values().stream()
.flatMap(Collection::stream)
.map(Bundle.BundleEntryComponent::getResource)
.forEach(resource -> {
if (resource instanceof ListResource) {
var listResource = (ListResource) resource;
if (isListSubmissionSet(listResource)) {
for (var entry : listResource.getEntry()) {
try {
expectedReferenceFullUrls.add(entry.getItem().getReference());
} catch (Exception ignored) {
}
}
}
patientReferences.add(getSubjectReference(resource, r -> listResource.getSubject()));
} else if (resource instanceof DocumentReference) {
var dr = (DocumentReference) resource;
for (var content : dr.getContent()) {
var url = content.getAttachment().getUrl();
if (!url.startsWith("http")) {
expectedBinaryFullUrls.add(url);
}
}
patientReferences.add(getSubjectReference(resource, r -> ((DocumentReference) r).getSubject()));
} else if (!(resource instanceof Binary)) {
throw FhirUtils.unprocessableEntity(
OperationOutcome.IssueSeverity.ERROR,
OperationOutcome.IssueType.INVALID,
null, null,
"Unexpected bundle component %s",
resource.getClass().getSimpleName()
);
}
});

if (patientReferences.size() != 1) {
throw FhirUtils.unprocessableEntity(
OperationOutcome.IssueSeverity.ERROR,
OperationOutcome.IssueType.INVALID,
ErrorCode.PATIENT_ID_DOES_NOT_MATCH.getOpcode(),
null,
"Inconsistent patient references %s",
patientReferences
);
}

entries.values().stream()
.flatMap(Collection::stream)
.forEach(entry -> {
if (ResourceType.DocumentReference == entry.getResource().getResourceType()) {
if (!expectedReferenceFullUrls.remove(entry.getFullUrl())) {
throw FhirUtils.unprocessableEntity(
OperationOutcome.IssueSeverity.ERROR,
OperationOutcome.IssueType.INVALID,
null, null,
"DocumentReference with URL %s is not referenced by any SubmissionSet list",
entry.getFullUrl()
);
}
} else if (ResourceType.Binary == entry.getResource().getResourceType()) {
if (!expectedBinaryFullUrls.remove(entry.getFullUrl())) {
throw FhirUtils.unprocessableEntity(
OperationOutcome.IssueSeverity.ERROR,
OperationOutcome.IssueType.INVALID,
null, null,
"Binary with URL %s is not referenced by any DocumentReference",
entry.getFullUrl()
);
}
}
});

if (!expectedBinaryFullUrls.isEmpty()) {
throw FhirUtils.unprocessableEntity(
OperationOutcome.IssueSeverity.ERROR,
OperationOutcome.IssueType.INVALID,
null, null,
"Binary with URLs %s referenced, but not present in this bundle",
expectedBinaryFullUrls
);
}

if (!expectedReferenceFullUrls.isEmpty()) {
throw FhirUtils.unprocessableEntity(
OperationOutcome.IssueSeverity.ERROR,
OperationOutcome.IssueType.INVALID,
null, null,
"DocumentReference with URLs %s referenced, but not present in this bundle",
expectedReferenceFullUrls
);
}

}

private boolean isListSubmissionSet(ListResource listResource) {
return COMPREHENSIVE_SUBMISSIONSET_TYPE_LIST.hasProfile(listResource) ||
MINIMAL_SUBMISSIONSET_TYPE_LIST.hasProfile(listResource) ||
UNCONTAINED_COMPREHENSIVE_SUBMISSIONSET_TYPE_LIST.hasProfile(listResource);
}

private String getSubjectReference(Resource resource, Function<Resource, Reference> f) {
var reference = f.apply(resource);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,8 @@
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.param.*;
import lombok.*;
import org.hl7.fhir.r4.model.DocumentManifest;
import org.hl7.fhir.r4.model.ListResource;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.PractitionerRole;
import org.openehealth.ipf.commons.ihe.fhir.FhirSearchAndSortParameters;

import java.util.*;
Expand All @@ -37,7 +36,7 @@
@Builder
@ToString
@AllArgsConstructor
public class Iti66ListSearchParameters extends FhirSearchAndSortParameters<DocumentManifest> {
public class Iti66ListSearchParameters extends FhirSearchAndSortParameters<ListResource> {

@Getter @Setter private DateRangeParam date;
@Getter @Setter private StringParam authorFamilyName;
Expand Down Expand Up @@ -83,32 +82,13 @@ public Iti66ListSearchParameters setAuthor(ReferenceAndListParam author) {
}

@Override
public Optional<Comparator<DocumentManifest>> comparatorFor(String paramName) {
if (DocumentManifest.SP_CREATED.equals(paramName)) {
return Optional.of(CP_CREATED);
} else if (DocumentManifest.SP_AUTHOR.equals(paramName)) {
return Optional.of(CP_AUTHOR);
public Optional<Comparator<ListResource>> comparatorFor(String paramName) {
if (ListResource.SP_DATE.equals(paramName)) {
return Optional.of(CP_DATE);
}
return Optional.empty();
}

private static final Comparator<DocumentManifest> CP_CREATED = nullsLast(comparing(DocumentManifest::getCreated));
private static final Comparator<ListResource> CP_DATE = nullsLast(comparing(ListResource::getDate));

private static final Comparator<DocumentManifest> CP_AUTHOR = nullsLast(comparing(documentManifest -> {
if (!documentManifest.hasAuthor()) return null;
var author = documentManifest.getAuthorFirstRep();
if (author.getResource() instanceof PractitionerRole) {
var practitionerRole = (PractitionerRole) author.getResource();
if (!practitionerRole.hasPractitioner()) return null;
author = practitionerRole.getPractitioner();
}
if (author.getResource() == null) return null;
if (author.getResource() instanceof Practitioner) {
var practitioner = (Practitioner) author.getResource();
if (!practitioner.hasName()) return null;
var name = practitioner.getNameFirstRep();
return name.getFamilyElement().getValueNotNull() + name.getGivenAsSingleString();
}
return null;
}));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openehealth.ipf.commons.ihe.fhir.mhd;

/**
* Marker interface for MHD 4.2.1 classes
*/
public interface Mhd421 extends MhdVersion {

@Override
default Version supportsVersion() {
return Version.v421;
}
}
Loading

0 comments on commit c706b43

Please sign in to comment.