diff --git a/dhis/src/main/java/org/dhis2/fhir/adapter/dhis/model/DhisResourceType.java b/dhis/src/main/java/org/dhis2/fhir/adapter/dhis/model/DhisResourceType.java index 30e669e9..8b6ca64a 100644 --- a/dhis/src/main/java/org/dhis2/fhir/adapter/dhis/model/DhisResourceType.java +++ b/dhis/src/main/java/org/dhis2/fhir/adapter/dhis/model/DhisResourceType.java @@ -47,16 +47,15 @@ public enum DhisResourceType */ ORGANIZATION_UNIT( "organisationUnits", "ou", "OrganizationUnitRule" ), - /** - * The program metadata. - */ - PROGRAM_METADATA( "programs", "pm", "ProgramMetadataRule" ), - /** * The program stage metadata. */ PROGRAM_STAGE_METADATA( "programStages", "sm", "ProgramStageMetadataRule" ), + /** + * The program metadata. + */ + PROGRAM_METADATA( "programs", "pm", "ProgramMetadataRule" ), /** * Resource is a tracked entity type. diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/model/FhirResourceType.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/model/FhirResourceType.java index 6bf41c17..ed0b04c6 100644 --- a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/model/FhirResourceType.java +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/model/FhirResourceType.java @@ -58,22 +58,22 @@ */ public enum FhirResourceType { - CONDITION( FhirVersion.ALL, "Condition", 19, Collections.emptySet(), Collections.singleton( "Condition" ) ), - DIAGNOSTIC_REPORT( FhirVersion.ALL, "DiagnosticReport", 30, Collections.emptySet(), Collections.singleton( "DiagnosticReport" ) ), - ENCOUNTER( FhirVersion.ALL, "Encounter", 4, Collections.emptySet(), Collections.singleton( "Encounter" ) ), - IMMUNIZATION( FhirVersion.ALL, "Immunization", 22, Collections.emptySet(), Collections.singleton( "Immunization" ) ), - LOCATION( FhirVersion.ALL, "Location", 2, Collections.emptySet(), Collections.singleton( "Location" ) ), - MEDICATION_REQUEST( FhirVersion.ALL, "MedicationRequest", 21, Collections.emptySet(), Collections.singleton( "MedicationRequest" ) ), - OBSERVATION( FhirVersion.ALL, "Observation", 20, Collections.emptySet(), Collections.singleton( "Observation" ) ), - ORGANIZATION( FhirVersion.ALL, "Organization", 1, Collections.emptySet(), Collections.singleton( "Organization" ) ), - PATIENT( FhirVersion.ALL, "Patient", 10, Collections.emptySet(), Collections.singleton( "Patient" ) ), - RELATED_PERSON( FhirVersion.ALL, "RelatedPerson", 11, Collections.emptySet(), Collections.singleton( "RelatedPerson" ) ), - PRACTITIONER( FhirVersion.ALL, "Practitioner", 9, Collections.emptySet(), Collections.singleton( "Practitioner" ) ), - MEASURE_REPORT( FhirVersion.ALL, "MeasureReport", 30, Collections.singleton( "MeasureReport" ), Collections.singleton( "MeasureReport" ) ), - PLAN_DEFINITION( FhirVersion.R4_ONLY, "PlanDefinition", 30, Collections.emptySet(), Collections.singleton( "PlanDefinition" ) ), - QUESTIONNAIRE( FhirVersion.R4_ONLY, "Questionnaire", 31, Collections.emptySet(), Collections.singleton( "Questionnaire" ) ), - CARE_PLAN( FhirVersion.R4_ONLY, "CarePlan", 35, Collections.emptySet(), Collections.singleton( "CarePlan" ) ), - QUESTIONNAIRE_RESPONSE( FhirVersion.R4_ONLY, "QuestionnaireResponse", 40, Collections.emptySet(), Collections.singleton( "QuestionnaireResponse" ) ); + CONDITION( FhirVersion.ALL, "Condition", false, 19, Collections.emptySet(), Collections.singleton( "Condition" ) ), + DIAGNOSTIC_REPORT( FhirVersion.ALL, "DiagnosticReport", false, 30, Collections.emptySet(), Collections.singleton( "DiagnosticReport" ) ), + ENCOUNTER( FhirVersion.ALL, "Encounter", false, 4, Collections.emptySet(), Collections.singleton( "Encounter" ) ), + IMMUNIZATION( FhirVersion.ALL, "Immunization", false, 22, Collections.emptySet(), Collections.singleton( "Immunization" ) ), + LOCATION( FhirVersion.ALL, "Location", false, 2, Collections.emptySet(), Collections.singleton( "Location" ) ), + MEDICATION_REQUEST( FhirVersion.ALL, "MedicationRequest", false, 21, Collections.emptySet(), Collections.singleton( "MedicationRequest" ) ), + OBSERVATION( FhirVersion.ALL, "Observation", false, 20, Collections.emptySet(), Collections.singleton( "Observation" ) ), + ORGANIZATION( FhirVersion.ALL, "Organization", false, 1, Collections.emptySet(), Collections.singleton( "Organization" ) ), + PATIENT( FhirVersion.ALL, "Patient", false, 10, Collections.emptySet(), Collections.singleton( "Patient" ) ), + RELATED_PERSON( FhirVersion.ALL, "RelatedPerson", false, 11, Collections.emptySet(), Collections.singleton( "RelatedPerson" ) ), + PRACTITIONER( FhirVersion.ALL, "Practitioner", false, 9, Collections.emptySet(), Collections.singleton( "Practitioner" ) ), + MEASURE_REPORT( FhirVersion.ALL, "MeasureReport", false, 30, Collections.singleton( "MeasureReport" ), Collections.singleton( "MeasureReport" ) ), + PLAN_DEFINITION( FhirVersion.R4_ONLY, "PlanDefinition", true, 30, Collections.emptySet(), Collections.singleton( "PlanDefinition" ) ), + QUESTIONNAIRE( FhirVersion.R4_ONLY, "Questionnaire", true, 31, Collections.emptySet(), Collections.singleton( "Questionnaire" ) ), + CARE_PLAN( FhirVersion.R4_ONLY, "CarePlan", false, 35, Collections.emptySet(), Collections.singleton( "CarePlan" ) ), + QUESTIONNAIRE_RESPONSE( FhirVersion.R4_ONLY, "QuestionnaireResponse", false, 40, Collections.emptySet(), Collections.singleton( "QuestionnaireResponse" ) ); private static final Map resourcesBySimpleClassName = Arrays.stream( values() ).flatMap( v -> v.getSimpleClassNames().stream().map( scn -> new SimpleEntry<>( scn, v ) ) ) .collect( Collectors.toMap( SimpleEntry::getKey, SimpleEntry::getValue ) ); @@ -85,17 +85,21 @@ public static FhirResourceType getByResource( @Nullable IBaseResource resource ) { return null; } + FhirResourceType frt; Class c = resource.getClass(); + do { frt = resourcesBySimpleClassName.get( c.getSimpleName() ); + if ( frt == null ) { c = c.getSuperclass(); } } while ( (frt == null) && (c != null) && (c != Object.class) ); + return frt; } @@ -115,6 +119,8 @@ public static FhirResourceType getByResourceTypeName( @Nullable String resourceT private final String resourceTypeName; + private final boolean syncDhisId; + private final int order; private final Set transactionalWith; @@ -123,10 +129,11 @@ public static FhirResourceType getByResourceTypeName( @Nullable String resourceT private volatile Set transactionalWithTypes; - FhirResourceType( Set fhirVersions, String resourceTypeName, int order, Collection transactionalWith, Collection simpleClassNames ) + FhirResourceType( Set fhirVersions, String resourceTypeName, boolean syncDhisId, int order, Collection transactionalWith, Collection simpleClassNames ) { this.fhirVersions = fhirVersions; this.resourceTypeName = resourceTypeName; + this.syncDhisId = syncDhisId; this.order = order; this.transactionalWith = new HashSet<>( transactionalWith ); this.simpleClassNames = Collections.unmodifiableSet( new HashSet<>( simpleClassNames ) ); @@ -144,6 +151,11 @@ public String getResourceTypeName() return resourceTypeName; } + public boolean isSyncDhisId() + { + return syncDhisId; + } + public int getOrder() { return order; diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/FhirResourceRepository.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/FhirResourceRepository.java index 48cfc16d..8ca0374a 100644 --- a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/FhirResourceRepository.java +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/FhirResourceRepository.java @@ -75,7 +75,7 @@ public interface FhirResourceRepository IBaseResource transform( @Nonnull UUID fhirClientId, @Nonnull FhirVersion fhirVersion, @Nullable IBaseResource resource ); @Nonnull - IBaseResource save( @Nonnull FhirClient fhirClient, @Nonnull IBaseResource resource ); + IBaseResource save( @Nonnull FhirClient fhirClient, @Nonnull IBaseResource resource, @Nullable String dhisResourceId ); boolean delete( @Nonnull FhirClient fhirClient, @Nonnull IBaseResource resource ); } diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/DhisRepositoryImpl.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/DhisRepositoryImpl.java index a613d7a2..b2c17a31 100644 --- a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/DhisRepositoryImpl.java +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/DhisRepositoryImpl.java @@ -451,7 +451,8 @@ protected boolean saveInternally( @Nonnull DhisResource resource ) } else { - final IBaseResource resultingResource = fhirResourceRepository.save( transformerRequest.getFhirClient(), outcome.getResource() ); + final IBaseResource resultingResource = fhirResourceRepository + .save( transformerRequest.getFhirClient(), outcome.getResource(), resource.getId() ); // resource may have been set as attribute in transformer context (e.g. shared encounter) outcome.getResource().setId( resultingResource.getIdElement() ); fhirDhisAssignmentRepository.saveFhirResourceId( outcome.getRule(), transformerRequest.getFhirClient(), @@ -482,6 +483,7 @@ public Optional read( @Nonnull FhirClient fhirClient, @Nonnull Fh if ( dhisResource == null ) { logger.debug( "DHIS resource could not be found." ); + return Optional.empty(); } @@ -493,6 +495,7 @@ public Optional read( @Nonnull FhirClient fhirClient, @Nonnull Fh if ( transformerRequest == null ) { logger.debug( "No matching rule has been found." ); + return Optional.empty(); } diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/FhirResourceRepositoryImpl.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/FhirResourceRepositoryImpl.java index 086f7b9b..fbbb7f7c 100644 --- a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/FhirResourceRepositoryImpl.java +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/FhirResourceRepositoryImpl.java @@ -276,7 +276,7 @@ public boolean delete( @Nonnull FhirClient fhirClient, @Nonnull IBaseResource re "T(org.dhis2.fhir.adapter.fhir.metadata.model.FhirResourceType).getByResource(#resource).getResourceTypeName(), #resource.getIdElement().getIdPart(), true}" ) @Nonnull @Override - public IBaseResource save( @Nonnull FhirClient fhirClient, @Nonnull IBaseResource resource ) + public IBaseResource save( @Nonnull FhirClient fhirClient, @Nonnull IBaseResource resource, @Nullable String dhisResourceId ) { if ( !fhirClient.getFhirEndpoint().isUseRemote() ) { @@ -287,25 +287,28 @@ public IBaseResource save( @Nonnull FhirClient fhirClient, @Nonnull IBaseResourc final FhirContext fhirContext = fhirContexts.get( fhirClient.getFhirVersion() ); final IGenericClient client = FhirClientUtils.createClient( fhirContext, fhirClient.getFhirEndpoint() ); + final IBaseResource preparedResource = prepareResource( resource, dhisResourceId ); final MethodOutcome methodOutcome; + if ( resource.getIdElement().hasIdPart() ) { try { - methodOutcome = client.update().resource( resource ).prefer( PreferReturnEnum.REPRESENTATION ).execute(); + methodOutcome = client.update().resource( preparedResource ).prefer( PreferReturnEnum.REPRESENTATION ).execute(); } catch ( PreconditionFailedException e ) { throw new OptimisticFhirResourceLockException( "Could not update FHIR resource " + - resource.getIdElement() + " because of an optimistic locking failure.", e ); + preparedResource.getIdElement() + " because of an optimistic locking failure.", e ); } } else { - methodOutcome = client.create().resource( resource ).prefer( PreferReturnEnum.REPRESENTATION ).execute(); + methodOutcome = client.create().resource( preparedResource ).prefer( PreferReturnEnum.REPRESENTATION ).execute(); } ProcessedItemInfo processedItemInfo = null; + if ( (methodOutcome.getResource() != null) && (methodOutcome.getResource().getMeta() != null) ) { // resource itself may contain old version ID (even if it should not) @@ -313,7 +316,7 @@ public IBaseResource save( @Nonnull FhirClient fhirClient, @Nonnull IBaseResourc } else if ( (methodOutcome.getId() != null) && methodOutcome.getId().hasVersionIdPart() ) { - processedItemInfo = ProcessedFhirItemInfoUtils.create( resource, methodOutcome.getId() ); + processedItemInfo = ProcessedFhirItemInfoUtils.create( preparedResource, methodOutcome.getId() ); } if ( processedItemInfo == null ) @@ -327,9 +330,11 @@ else if ( (methodOutcome.getId() != null) && methodOutcome.getId().hasVersionIdP } final IBaseResource result; + if ( methodOutcome.getResource() == null ) { result = resource; + if ( methodOutcome.getId() != null ) { result.setId( methodOutcome.getId() ); @@ -343,6 +348,24 @@ else if ( (methodOutcome.getId() != null) && methodOutcome.getId().hasVersionIdP return result; } + @Nonnull + protected T prepareResource( @Nonnull T resource, @Nullable String dhisResourceId ) + { + final FhirResourceType fhirResourceType = FhirResourceType.getByResource( resource ); + + if ( fhirResourceType == null ) + { + throw new FhirResourceTransformationException( "Could not determine FHIR resource type for " + resource.getClass().getSimpleName() ); + } + + if ( fhirResourceType.isSyncDhisId() && dhisResourceId != null && !resource.getIdElement().hasIdPart() ) + { + resource.setId( dhisResourceId ); + } + + return resource; + } + @TransactionalEventListener( phase = TransactionPhase.BEFORE_COMMIT, classes = AutoCreatedFhirClientResourceEvent.class ) public void autoCreatedSubscriptionResource( @Nonnull AutoCreatedFhirClientResourceEvent event ) { diff --git a/fhir/src/test/java/org/dhis2/fhir/adapter/fhir/repository/impl/FhirResourceRepositoryImplTest.java b/fhir/src/test/java/org/dhis2/fhir/adapter/fhir/repository/impl/FhirResourceRepositoryImplTest.java new file mode 100644 index 00000000..6fcd4ef9 --- /dev/null +++ b/fhir/src/test/java/org/dhis2/fhir/adapter/fhir/repository/impl/FhirResourceRepositoryImplTest.java @@ -0,0 +1,207 @@ +package org.dhis2.fhir.adapter.fhir.repository.impl; + +/* + * Copyright (c) 2004-2019, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.model.api.IFhirVersion; +import ca.uhn.fhir.model.primitive.IdDt; +import org.dhis2.fhir.adapter.fhir.client.StoredFhirResourceService; +import org.dhis2.fhir.adapter.fhir.metadata.repository.FhirClientResourceRepository; +import org.dhis2.fhir.adapter.fhir.script.ScriptExecutor; +import org.dhis2.fhir.adapter.spring.StaticObjectProvider; +import org.hl7.fhir.instance.model.api.IBaseMetaType; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Implementation of {@link FhirResourceRepositoryImpl}. + * + * @author volsch + */ +public class FhirResourceRepositoryImplTest +{ + @Mock + private ScriptExecutor scriptExecutor; + + @Mock + private StoredFhirResourceService storedItemService; + + @Mock + private FhirClientResourceRepository fhirClientResourceRepository; + + @Mock + private FhirContext fhirContextDstu3; + + @Mock + private IFhirVersion fhirVersionDstu3; + + @Mock + private FhirContext fhirContextR4; + + @Mock + private IFhirVersion fhirVersionR4; + + private FhirResourceRepositoryImpl repository; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Before + public void setUp() + { + Mockito.when( fhirContextDstu3.getVersion() ).thenReturn( fhirVersionDstu3 ); + Mockito.when( fhirVersionDstu3.getVersion() ).thenReturn( FhirVersionEnum.DSTU3 ); + Mockito.when( fhirContextR4.getVersion() ).thenReturn( fhirVersionR4 ); + Mockito.when( fhirVersionR4.getVersion() ).thenReturn( FhirVersionEnum.R4 ); + + repository = new FhirResourceRepositoryImpl( scriptExecutor, storedItemService, fhirClientResourceRepository, + new StaticObjectProvider<>( Arrays.asList( fhirContextDstu3, fhirContextR4 ) ), new StaticObjectProvider<>( Collections.emptyList() ) ); + } + + @Test + public void prepareResourceWithoutAnyId() + { + Patient patient = new Patient(); + patient = repository.prepareResource( patient, null ); + Assert.assertTrue( patient.getIdElement().isEmpty() ); + } + + @Test + public void prepareResourceNonMatchingWithDhisId() + { + Patient patient = new Patient(); + patient = repository.prepareResource( patient, "a0123456789" ); + Assert.assertTrue( patient.getIdElement().isEmpty() ); + } + + @Test + public void prepareResourceWithDhisId() + { + PlanDefinition planDefinition = new PlanDefinition(); + planDefinition = repository.prepareResource( planDefinition, "a0123456789" ); + Assert.assertEquals( "a0123456789", planDefinition.getIdElement().getIdPart() ); + } + + @Test + public void prepareResourceWithAllIds() + { + PlanDefinition planDefinition = new PlanDefinition(); + planDefinition.setId( "d0123456789" ); + planDefinition = repository.prepareResource( planDefinition, "a0123456789" ); + Assert.assertEquals( "d0123456789", planDefinition.getIdElement().getIdPart() ); + } + + public static class Patient extends AbstractBaseResource + { + private static final long serialVersionUID = -1428885428508171576L; + } + + public static class PlanDefinition extends AbstractBaseResource + { + private static final long serialVersionUID = -1428885428508171576L; + } + + public static abstract class AbstractBaseResource implements IBaseResource + { + private static final long serialVersionUID = 7566894473303706042L; + + private IIdType id = new IdDt(); + + @Override + public IBaseMetaType getMeta() + { + return null; + } + + @Override + public IIdType getIdElement() + { + return id; + } + + @Override + public IBaseResource setId( String theId ) + { + id = new IdDt( theId ); + + return this; + } + + @Override + public IBaseResource setId( IIdType theId ) + { + this.id = theId; + + return this; + } + + @Override + public FhirVersionEnum getStructureFhirVersionEnum() + { + return FhirVersionEnum.R4; + } + + @Override + public boolean isEmpty() + { + return false; + } + + @Override + public boolean hasFormatComment() + { + return false; + } + + @Override + public List getFormatCommentsPre() + { + return null; + } + + @Override + public List getFormatCommentsPost() + { + return null; + } + } +} \ No newline at end of file