diff --git a/commons/ihe/fhir/r4/chppqm/src/main/groovy/org/openehealth/ipf/commons/ihe/fhir/chppqm/translation/FhirToXacmlTranslator.groovy b/commons/ihe/fhir/r4/chppqm/src/main/groovy/org/openehealth/ipf/commons/ihe/fhir/chppqm/translation/FhirToXacmlTranslator.groovy index 3d16aac8f4..9d8101c85f 100644 --- a/commons/ihe/fhir/r4/chppqm/src/main/groovy/org/openehealth/ipf/commons/ihe/fhir/chppqm/translation/FhirToXacmlTranslator.groovy +++ b/commons/ihe/fhir/r4/chppqm/src/main/groovy/org/openehealth/ipf/commons/ihe/fhir/chppqm/translation/FhirToXacmlTranslator.groovy @@ -66,6 +66,7 @@ class FhirToXacmlTranslator { def templateId = ChPpqmUtils.extractConsentId(consent, ChPpqmUtils.ConsentIdTypes.TEMPLATE_ID) switch (templateId) { case '301': + case '304': substitutions.put('gln', consent.provision.actor[0].reference.identifier.value) break case '302': diff --git a/commons/ihe/fhir/r4/chppqm/src/main/groovy/org/openehealth/ipf/commons/ihe/fhir/chppqm/translation/XacmlToFhirTranslator.groovy b/commons/ihe/fhir/r4/chppqm/src/main/groovy/org/openehealth/ipf/commons/ihe/fhir/chppqm/translation/XacmlToFhirTranslator.groovy index e848ab9375..46aff977ad 100644 --- a/commons/ihe/fhir/r4/chppqm/src/main/groovy/org/openehealth/ipf/commons/ihe/fhir/chppqm/translation/XacmlToFhirTranslator.groovy +++ b/commons/ihe/fhir/r4/chppqm/src/main/groovy/org/openehealth/ipf/commons/ihe/fhir/chppqm/translation/XacmlToFhirTranslator.groovy @@ -96,7 +96,9 @@ class XacmlToFhirTranslator { } String gln = extractAttributeValue(subjectMatches, 'urn:oasis:names:tc:xacml:1.0:subject:subject-id') - return create301Consent(id, eprSpid, gln, policyIdReference, startDate, endDate) + return policyIdReference.contains('delegation') + ? create304Consent(id, eprSpid, gln, policyIdReference, startDate, endDate) + : create301Consent(id, eprSpid, gln, policyIdReference, startDate, endDate) } private static Date parseDate(GPathResult xacml, String matchId) { diff --git a/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/ChPpqmConsentCreator.java b/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/ChPpqmConsentCreator.java index aed3d574e8..c56cb8c876 100644 --- a/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/ChPpqmConsentCreator.java +++ b/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/ChPpqmConsentCreator.java @@ -82,7 +82,7 @@ private static Consent createConsent( .collect(Collectors.toList()))); consent.setId(consentId); - consent.getMeta().addProfile(ChPpqmUtils.Profiles.CONSENT); + consent.getMeta().addProfile(ChPpqmUtils.getTemplateProfileUri(templateId)); return consent; } @@ -110,7 +110,8 @@ private static Consent.provisionActorComponent createActor(SubjectRole role) { private static Consent.provisionActorComponent createInstanceActor( SubjectRole role, String idQualifier, - String id) + String id, + String idSystem) { return createActor(role) .setReference(new Reference() @@ -118,6 +119,7 @@ private static Consent.provisionActorComponent createInstanceActor( .setType(new CodeableConcept(new Coding() .setSystem(Constants.URN_IETF_RFC_3986) .setCode(idQualifier))) + .setSystem(idSystem) .setValue(id))); } @@ -137,7 +139,7 @@ public static Consent create201Consent(String id, String eprSpid) { "201", eprSpid, "urn:e-health-suisse:2015:policies:access-level:full", - createInstanceActor(SubjectRole.PATIENT, "urn:e-health-suisse:2015:epr-spid", eprSpid), + createInstanceActor(SubjectRole.PATIENT, "urn:e-health-suisse:2015:epr-spid", eprSpid, "urn:oid:" + PpqConstants.CodingSystemIds.SWISS_PATIENT_ID), null, null, Collections.emptyList()); @@ -169,7 +171,7 @@ public static Consent create203Consent(String id, String eprSpid, String policyI List.of(PurposeOfUse.NORMAL, PurposeOfUse.AUTO, PurposeOfUse.DICOM_AUTO)); } - // template 301 -- read access for a particular HCP + // template 301 -- read access for a particular HCP without the possibility to delegate public static Consent create301Consent( String id, String eprSpid, @@ -178,16 +180,12 @@ public static Consent create301Consent( Date startDate, Date endDate) { - if (policyIdReference.contains("delegation") && (endDate == null)) { - // TODO: In Swiss EPR spec revision 2024, delegation will be moved to template 304. - throw new IllegalArgumentException("In delegation policies, the end date shall be provided"); - } return createConsent( id, "301", eprSpid, policyIdReference, - createInstanceActor(SubjectRole.PROFESSIONAL, "urn:gs1:gln", gln), + createInstanceActor(SubjectRole.PROFESSIONAL, "urn:gs1:gln", gln, ChPpqmUtils.CodingSystems.GLN), startDate, endDate, List.of(PurposeOfUse.NORMAL)); @@ -210,7 +208,7 @@ public static Consent create302Consent( "302", eprSpid, policyIdReference, - createInstanceActor(SubjectRole.PROFESSIONAL, "urn:oasis:names:tc:xspa:1.0:subject:organization-id", groupOid), + createInstanceActor(SubjectRole.PROFESSIONAL, "urn:oasis:names:tc:xspa:1.0:subject:organization-id", groupOid, null), startDate, endDate, List.of(PurposeOfUse.NORMAL)); @@ -229,10 +227,33 @@ public static Consent create303Consent( "303", eprSpid, "urn:e-health-suisse:2015:policies:access-level:full", - createInstanceActor(SubjectRole.REPRESENTATIVE, "urn:e-health-suisse:representative-id", representativeId), + createInstanceActor(SubjectRole.REPRESENTATIVE, "urn:e-health-suisse:representative-id", representativeId, null), startDate, endDate, Collections.emptyList()); } + // template 304 -- read access for a particular HCP, with the possibility to delegate + public static Consent create304Consent( + String id, + String eprSpid, + String gln, + String policyIdReference, + Date startDate, + Date endDate) + { + if (policyIdReference.contains("delegation") && (endDate == null)) { + throw new IllegalArgumentException("In delegation policies, the end date shall be provided"); + } + return createConsent( + id, + "304", + eprSpid, + policyIdReference, + createInstanceActor(SubjectRole.PROFESSIONAL, "urn:gs1:gln", gln, ChPpqmUtils.CodingSystems.GLN), + startDate, + endDate, + List.of(PurposeOfUse.NORMAL)); + } + } diff --git a/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/ChPpqmUtils.java b/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/ChPpqmUtils.java index 553e986098..208a5a97ea 100644 --- a/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/ChPpqmUtils.java +++ b/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/ChPpqmUtils.java @@ -44,8 +44,9 @@ public class ChPpqmUtils { try { FHIR_CONTEXT = IgBasedFhirContextSupplier.getContext( FhirContext.forR4(), - "classpath:/igs/ch-ppqm-2.0.0.tgz", - "classpath:/igs/ch-epr-term-2.0.9.tgz"); + "classpath:/igs/ch-epr-fhir-4.0.1-ballot.tgz", + "classpath:/igs/ch-epr-term-2.0.9.tgz", + "classpath:/igs/ch-core-4.0.1.tgz"); } catch (IOException e) { throw new ExceptionInInitializerError(e); } @@ -55,14 +56,25 @@ public static FhirContext getFhirContext() { return FHIR_CONTEXT; } + public static final Set TEMPLATE_IDS = Set.of("201", "202", "203", "301", "302", "303", "304"); + + public static String getTemplateProfileUri(String templateId) { + return Profiles.BASE_URL + "/PpqmConsentTemplate" + templateId; + } + + public static final Set TEMPLATE_PROFILE_URIS = TEMPLATE_IDS.stream() + .map(ChPpqmUtils::getTemplateProfileUri) + .collect(Collectors.toSet()); + public static class Profiles { - public static final String CONSENT = "http://fhir.ch/ig/ch-epr-ppqm/StructureDefinition/PpqmConsent"; - public static final String FEED_REQUEST_BUNDLE = "http://fhir.ch/ig/ch-epr-ppqm/StructureDefinition/PpqmFeedRequestBundle"; - public static final String RETRIEVE_RESPONSE_BUNDLE = "http://fhir.ch/ig/ch-epr-ppqm/StructureDefinition/PpqmRetrieveResponseBundle"; + public static final String BASE_URL = "http://fhir.ch/ig/ch-epr-fhir/StructureDefinition"; + public static final String FEED_REQUEST_BUNDLE = BASE_URL + "/PpqmFeedRequestBundle"; + public static final String RETRIEVE_RESPONSE_BUNDLE = BASE_URL + "/PpqmRetrieveResponseBundle"; } public static class CodingSystems { - public static String CONSENT_IDENTIFIER_TYPE = "http://fhir.ch/ig/ch-epr-ppqm/CodeSystem/PpqmConsentIdentifierType"; + public static String CONSENT_IDENTIFIER_TYPE = "http://fhir.ch/ig/ch-epr-fhir/CodeSystem/PpqmConsentIdentifierType"; + public static String GLN = "urn:oid:2.51.1.3"; } public static class ConsentIdTypes { diff --git a/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/chppq3/ChPpq3Validator.java b/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/chppq3/ChPpq3Validator.java index ea62922294..5cf3ff44b8 100644 --- a/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/chppq3/ChPpq3Validator.java +++ b/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/chppq3/ChPpq3Validator.java @@ -42,7 +42,7 @@ private OperationOutcome doValidateRequest(Object payload, Map p switch (method) { case "POST": case "PUT": - return validateProfileConformance((Resource) payload, ChPpqmUtils.Profiles.CONSENT); + return validateProfileConformance((Resource) payload, ChPpqmUtils.TEMPLATE_PROFILE_URIS); case "DELETE": String resourceId = (String) payload; diff --git a/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/chppq5/ChPpq5Validator.java b/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/chppq5/ChPpq5Validator.java index b379dee66e..02ef8d2826 100644 --- a/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/chppq5/ChPpq5Validator.java +++ b/commons/ihe/fhir/r4/chppqm/src/main/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/chppq5/ChPpq5Validator.java @@ -55,7 +55,7 @@ public void validateRequest(Object payload, Map parameters) { @Override public void validateResponse(Object payload, Map parameters) { - handleOperationOutcome(validateProfileConformance((Resource) payload, ChPpqmUtils.Profiles.CONSENT)); + handleOperationOutcome(validateProfileConformance((Resource) payload, ChPpqmUtils.TEMPLATE_PROFILE_URIS)); } } diff --git a/commons/ihe/fhir/r4/chppqm/src/main/resources/igs/ch-core-4.0.1.tgz b/commons/ihe/fhir/r4/chppqm/src/main/resources/igs/ch-core-4.0.1.tgz new file mode 100644 index 0000000000..bc5aed9ba4 Binary files /dev/null and b/commons/ihe/fhir/r4/chppqm/src/main/resources/igs/ch-core-4.0.1.tgz differ diff --git a/commons/ihe/fhir/r4/chppqm/src/main/resources/igs/ch-epr-fhir-4.0.1-ballot.tgz b/commons/ihe/fhir/r4/chppqm/src/main/resources/igs/ch-epr-fhir-4.0.1-ballot.tgz new file mode 100644 index 0000000000..529cd102e9 Binary files /dev/null and b/commons/ihe/fhir/r4/chppqm/src/main/resources/igs/ch-epr-fhir-4.0.1-ballot.tgz differ diff --git a/commons/ihe/fhir/r4/chppqm/src/main/resources/igs/ch-ppqm-2.0.0.tgz b/commons/ihe/fhir/r4/chppqm/src/main/resources/igs/ch-ppqm-2.0.0.tgz deleted file mode 100644 index af7d6b755f..0000000000 Binary files a/commons/ihe/fhir/r4/chppqm/src/main/resources/igs/ch-ppqm-2.0.0.tgz and /dev/null differ diff --git a/commons/ihe/fhir/r4/chppqm/src/test/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/TranslationTest.java b/commons/ihe/fhir/r4/chppqm/src/test/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/TranslationTest.java index 84585eb5fd..c917951100 100644 --- a/commons/ihe/fhir/r4/chppqm/src/test/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/TranslationTest.java +++ b/commons/ihe/fhir/r4/chppqm/src/test/java/org/openehealth/ipf/commons/ihe/fhir/chppqm/TranslationTest.java @@ -127,7 +127,7 @@ public void testConsent203Creation1() throws Exception { @Test public void testConsent301Creation1() throws Exception { Consent consent = create301Consent(createUuid(), "123456789012345678", "3210987654321", - "urn:e-health-suisse:2015:policies:access-level:delegation-and-normal", null, new Date()); + "urn:e-health-suisse:2015:policies:access-level:normal", null, new Date()); doTest("301", consent, "POST", null); } @@ -144,6 +144,13 @@ public void testConsent303Creation1() throws Exception { doTest("303", consent, "POST", null); } + @Test + public void testConsent304Creation1() throws Exception { + Consent consent = create304Consent(createUuid(), "123456789012345678", "3210987654321", + "urn:e-health-suisse:2015:policies:access-level:delegation-and-normal", null, new Date()); + doTest("304", consent, "POST", null); + } + @Test public void testPpq3To1RequestTranslation() { Consent consent = create303Consent(createUuid(), "123456789012345678", "rep123", null, null); diff --git a/commons/ihe/fhir/r4/core/src/main/java/org/openehealth/ipf/commons/ihe/fhir/support/IgBasedInstanceValidator.java b/commons/ihe/fhir/r4/core/src/main/java/org/openehealth/ipf/commons/ihe/fhir/support/IgBasedInstanceValidator.java index c93cd5b9e5..e35452448f 100644 --- a/commons/ihe/fhir/r4/core/src/main/java/org/openehealth/ipf/commons/ihe/fhir/support/IgBasedInstanceValidator.java +++ b/commons/ihe/fhir/r4/core/src/main/java/org/openehealth/ipf/commons/ihe/fhir/support/IgBasedInstanceValidator.java @@ -20,6 +20,7 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ValidationResult; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.r4.model.CanonicalType; import org.hl7.fhir.r4.model.OperationOutcome; @@ -27,6 +28,7 @@ import org.openehealth.ipf.commons.ihe.fhir.FhirTransactionValidator; import java.util.Comparator; +import java.util.Set; /** * Validator which uses Implementation Guides to validate FHIR resources. @@ -46,8 +48,8 @@ protected IgBasedInstanceValidator(FhirContext fhirContext) { * @param resource FHIR resource to validate. * @return {@link OperationOutcome} containing or not containing validation errors (never null). */ - protected OperationOutcome validateProfileConformance(Resource resource, String profileUri) { - + protected OperationOutcome validateProfileConformance(Resource resource, Set allowedProfileUris) { + String profileUri = allowedProfileUris.iterator().next(); if (profileUri.startsWith(STANDARD_PREFIX)) { String expectedResourceType = profileUri.substring(STANDARD_PREFIX.length()); if (resource.fhirType().equals(expectedResourceType)) { @@ -61,7 +63,7 @@ protected OperationOutcome validateProfileConformance(Resource resource, String } } else { for (CanonicalType profile : resource.getMeta().getProfile()) { - if (profile.equals(profileUri)) { + if (allowedProfileUris.contains(profile.asStringValue())) { return doValidate(resource); } } @@ -71,7 +73,11 @@ protected OperationOutcome validateProfileConformance(Resource resource, String .addIssue(new OperationOutcome.OperationOutcomeIssueComponent() .setSeverity(OperationOutcome.IssueSeverity.ERROR) .setCode(OperationOutcome.IssueType.REQUIRED) - .setDiagnostics("Resource shall declare profile " + profileUri)); + .setDiagnostics("Resource shall declare one of the profiles: " + StringUtils.join(allowedProfileUris, ", "))); + } + + protected OperationOutcome validateProfileConformance(Resource resource, String allowedProfileUri) { + return validateProfileConformance(resource, Set.of(allowedProfileUri)); } private OperationOutcome doValidate(Resource resource) { diff --git a/commons/ihe/xacml20/impl/src/main/groovy/org/openehealth/ipf/commons/ihe/xacml20/ChPpqPolicySetCreator.groovy b/commons/ihe/xacml20/impl/src/main/groovy/org/openehealth/ipf/commons/ihe/xacml20/ChPpqPolicySetCreator.groovy index 5a628c5aa5..be8bfa3249 100644 --- a/commons/ihe/xacml20/impl/src/main/groovy/org/openehealth/ipf/commons/ihe/xacml20/ChPpqPolicySetCreator.groovy +++ b/commons/ihe/xacml20/impl/src/main/groovy/org/openehealth/ipf/commons/ihe/xacml20/ChPpqPolicySetCreator.groovy @@ -35,7 +35,7 @@ class ChPpqPolicySetCreator { velocityProperties.setProperty('resource.loader.classpath.class', ClasspathResourceLoader.class.getName()); VELOCITY = new VelocityEngine(velocityProperties) VELOCITY.init() - POLICY_SET_TEMPLATES = ['201', '202', '203', '301', '302', '303'].collectEntries { templateId -> + POLICY_SET_TEMPLATES = ['201', '202', '203', '301', '302', '303', '304'].collectEntries { templateId -> [templateId, VELOCITY.getTemplate("templates/policy-set-template-${templateId}.xml")] } } diff --git a/commons/ihe/xacml20/impl/src/main/resources/templates/policy-set-template-301.xml b/commons/ihe/xacml20/impl/src/main/resources/templates/policy-set-template-301.xml index b659927d76..a57688e7cc 100644 --- a/commons/ihe/xacml20/impl/src/main/resources/templates/policy-set-template-301.xml +++ b/commons/ihe/xacml20/impl/src/main/resources/templates/policy-set-template-301.xml @@ -48,7 +48,6 @@ #if ($startDate || $endDate) - #if ($startDate) diff --git a/commons/ihe/xacml20/impl/src/main/resources/templates/policy-set-template-304.xml b/commons/ihe/xacml20/impl/src/main/resources/templates/policy-set-template-304.xml new file mode 100644 index 0000000000..eab00857fa --- /dev/null +++ b/commons/ihe/xacml20/impl/src/main/resources/templates/policy-set-template-304.xml @@ -0,0 +1,89 @@ + + + + Patient specific PolicySet for User Assignment 304 - allowing a user (health professional) to access the patient's EPD according to the scope of the referenced access level (PolicySetIdReference below) + with the possibility of delegation + + + + + + ${gln} + + + + urn:gs1:gln + + + + + + + + + + + + + + + + + + + #if ($startDate) + + ${startDate} + + + #end + + ${endDate} + + + + + + + #if ($startDate) + + ${startDate} + + + #end + + ${endDate} + + + + + + + ${policyIdReference} + +