From b8ed06e847028b58fee99279b1a7a2013bbc3d56 Mon Sep 17 00:00:00 2001 From: Volker Schmidt Date: Wed, 30 Jan 2019 18:11:37 +0100 Subject: [PATCH] Begin of support for remote synchronization (e.g. OpenMRS Sync2). --- .../dhis2/fhir/adapter/WebSecurityConfig.java | 6 +- .../dhis2/fhir/adapter/util/NameUtils.java | 84 ++++++++++ .../Dstu3FhirResourceRepositoryImpl.java | 7 +- .../fhir/metadata/model/FhirResourceType.java | 8 +- .../fhir/metadata/model/FhirServer.java | 16 +- .../metadata/model/FhirServerResource.java | 30 +++- .../FhirServerResourceRepository.java | 14 +- .../FhirServerSystemRepository.java | 14 +- ...CreateSaveFhirServerResourceValidator.java | 36 +++- .../repository/FhirResourceRepository.java | 5 +- .../AbstractFhirResourceRepositoryImpl.java | 67 ++++++-- .../impl/AbstractRepositoryResourceUtils.java | 64 +++++++ .../server/AbstractFhirServerController.java | 144 ++++++++++++++++ .../server/FhirServerRestHookController.java | 79 +-------- .../fhir/server/FhirServerSyncController.java | 158 ++++++++++++++++++ ....6_21_0__OpenMRS_Advanced_Subscription.sql | 34 ++++ .../V1.1.0.6_0_0__Advanced_Subscription.sql | 11 ++ .../FhirServerRepositoryRestDocsTest.java | 6 +- ...rServerResourceRepositoryRestDocsTest.java | 13 +- pom.xml | 6 + 20 files changed, 694 insertions(+), 108 deletions(-) create mode 100644 common/src/main/java/org/dhis2/fhir/adapter/util/NameUtils.java create mode 100644 fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/AbstractRepositoryResourceUtils.java create mode 100644 fhir/src/main/java/org/dhis2/fhir/adapter/fhir/server/AbstractFhirServerController.java create mode 100644 fhir/src/main/java/org/dhis2/fhir/adapter/fhir/server/FhirServerSyncController.java create mode 100644 fhir/src/main/resources/db/migration/openmrs/V1.1.0.6_21_0__OpenMRS_Advanced_Subscription.sql diff --git a/app/src/main/java/org/dhis2/fhir/adapter/WebSecurityConfig.java b/app/src/main/java/org/dhis2/fhir/adapter/WebSecurityConfig.java index cb87d787..01d547b0 100644 --- a/app/src/main/java/org/dhis2/fhir/adapter/WebSecurityConfig.java +++ b/app/src/main/java/org/dhis2/fhir/adapter/WebSecurityConfig.java @@ -1,7 +1,7 @@ package org.dhis2.fhir.adapter; /* - * Copyright (c) 2004-2018, University of Oslo + * Copyright (c) 2004-2019, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -69,6 +69,10 @@ protected void configure( @Nonnull HttpSecurity http ) throws Exception .authorizeRequests() .antMatchers( HttpMethod.PUT, "/remote-fhir-rest-hook/**" ).permitAll() .antMatchers( HttpMethod.POST, "/remote-fhir-rest-hook/**" ).permitAll() + .antMatchers( HttpMethod.PUT, "/remote-fhir-sync/**" ).permitAll() + .antMatchers( HttpMethod.POST, "/remote-fhir-sync/**" ).permitAll() + .antMatchers( HttpMethod.DELETE, "/remote-fhir-sync/**" ).permitAll() + .antMatchers( HttpMethod.GET, "/remote-fhir-sync/**" ).permitAll() .antMatchers( HttpMethod.GET, "/favicon.ico" ).permitAll() .antMatchers( HttpMethod.GET, "/actuator/health" ).permitAll() .antMatchers( HttpMethod.GET, "/actuator/info" ).permitAll() diff --git a/common/src/main/java/org/dhis2/fhir/adapter/util/NameUtils.java b/common/src/main/java/org/dhis2/fhir/adapter/util/NameUtils.java new file mode 100644 index 00000000..d473fc45 --- /dev/null +++ b/common/src/main/java/org/dhis2/fhir/adapter/util/NameUtils.java @@ -0,0 +1,84 @@ +package org.dhis2.fhir.adapter.util; + +/* + * 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 com.google.common.base.CaseFormat; +import org.apache.commons.lang.StringUtils; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Name utilities for converting to enum constants and class names. + */ +public abstract class NameUtils +{ + @Nullable + public static String toClassName( @Nullable Object value ) + { + final String stringValue = toEnumName( value ); + if ( stringValue == null ) + { + return null; + } + return CaseFormat.UPPER_UNDERSCORE.to( CaseFormat.UPPER_CAMEL, stringValue ); + } + + @Nonnull + public static > Enum getEnumValue( @Nonnull Class enumClass, @Nullable Object value ) throws IllegalArgumentException + { + final String stringValue = toEnumName( value ); + if ( stringValue == null ) + { + throw new IllegalArgumentException( "Null enum values are not allowed" ); + } + return Enum.valueOf( enumClass, stringValue ); + } + + @Nullable + public static String toEnumName( @Nullable Object value ) + { + if ( value == null ) + { + return null; + } + final String stringValue = value.toString(); + if ( stringValue == null ) + { + return null; + } + if ( stringValue.length() == 0 ) + { + return stringValue; + } + + return StringUtils.join( StringUtils.splitByCharacterTypeCamelCase( stringValue ), '_' ) + .replace( '-', '_' ).toUpperCase(); + } +} diff --git a/fhir-dstu3/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/dstu3/Dstu3FhirResourceRepositoryImpl.java b/fhir-dstu3/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/dstu3/Dstu3FhirResourceRepositoryImpl.java index f4b1010f..c65d0091 100644 --- a/fhir-dstu3/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/dstu3/Dstu3FhirResourceRepositoryImpl.java +++ b/fhir-dstu3/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/dstu3/Dstu3FhirResourceRepositoryImpl.java @@ -1,7 +1,7 @@ package org.dhis2.fhir.adapter.fhir.repository.impl.dstu3; /* - * Copyright (c) 2004-2018, University of Oslo + * Copyright (c) 2004-2019, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,6 +31,7 @@ import ca.uhn.fhir.context.FhirContext; import org.dhis2.fhir.adapter.fhir.metadata.model.FhirServerResource; import org.dhis2.fhir.adapter.fhir.metadata.model.SubscriptionType; +import org.dhis2.fhir.adapter.fhir.metadata.repository.FhirServerResourceRepository; import org.dhis2.fhir.adapter.fhir.repository.impl.AbstractFhirResourceRepositoryImpl; import org.dhis2.fhir.adapter.fhir.server.StoredFhirResourceService; import org.hl7.fhir.dstu3.model.Bundle; @@ -53,9 +54,9 @@ @Component public class Dstu3FhirResourceRepositoryImpl extends AbstractFhirResourceRepositoryImpl { - public Dstu3FhirResourceRepositoryImpl( @Nonnull StoredFhirResourceService storedItemService, @Nonnull ObjectProvider> fhirContexts ) + public Dstu3FhirResourceRepositoryImpl( @Nonnull StoredFhirResourceService storedItemService, @Nonnull FhirServerResourceRepository fhirServerResourceRepository, @Nonnull ObjectProvider> fhirContexts ) { - super( storedItemService, fhirContexts ); + super( storedItemService, fhirServerResourceRepository, fhirContexts ); } @Nonnull 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 87932d44..bc775c98 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 @@ -1,7 +1,7 @@ package org.dhis2.fhir.adapter.fhir.metadata.model; /* - * Copyright (c) 2004-2018, University of Oslo + * Copyright (c) 2004-2019, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -88,6 +88,12 @@ public static FhirResourceType getByResource( @Nullable IBaseResource resource ) return frt; } + @Nullable + public static FhirResourceType getByResourceTypeName( @Nullable String resourceTypeName ) + { + return resourcesBySimpleClassName.get( resourceTypeName ); + } + private final String resourceTypeName; private final int order; diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/model/FhirServer.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/model/FhirServer.java index a0229c54..b367370f 100644 --- a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/model/FhirServer.java +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/model/FhirServer.java @@ -1,7 +1,7 @@ package org.dhis2.fhir.adapter.fhir.metadata.model; /* - * Copyright (c) 2004-2018, University of Oslo + * Copyright (c) 2004-2019, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -105,6 +105,8 @@ public class FhirServer extends VersionedBaseMetadata implements DataGroup, Seri @Min( 0 ) private int toleranceMillis; + private boolean remoteSyncEnabled; + @NotNull @Valid private SubscriptionDhisEndpoint dhisEndpoint; @@ -220,6 +222,18 @@ public void setToleranceMillis( int toleranceMillis ) this.toleranceMillis = toleranceMillis; } + @Basic + @Column( name = "remote_sync_enabled", nullable = false, columnDefinition = "BOOLEAN DEFAULT FALSE NOT NULL" ) + public boolean isRemoteSyncEnabled() + { + return remoteSyncEnabled; + } + + public void setRemoteSyncEnabled( boolean remoteSyncEnabled ) + { + this.remoteSyncEnabled = remoteSyncEnabled; + } + @Basic @Column( name = "fhir_version", nullable = false ) @Enumerated( EnumType.STRING ) diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/model/FhirServerResource.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/model/FhirServerResource.java index 3062bc04..d7fa4b9b 100644 --- a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/model/FhirServerResource.java +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/model/FhirServerResource.java @@ -1,7 +1,7 @@ package org.dhis2.fhir.adapter.fhir.metadata.model; /* - * Copyright (c) 2004-2018, University of Oslo + * Copyright (c) 2004-2019, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -89,6 +89,10 @@ public class FhirServerResource extends VersionedBaseMetadata implements DataGro private String fhirSubscriptionId; + private ExecutableScript impTransformScript; + + private boolean preferred; + @Basic @Column( name = "fhir_resource_type", nullable = false, length = 30 ) @Enumerated( EnumType.STRING ) @@ -188,6 +192,30 @@ public void setFhirSubscriptionId( String fhirSubscriptionId ) this.fhirSubscriptionId = fhirSubscriptionId; } + @ManyToOne + @JoinColumn( name = "imp_transform_script_id" ) + public ExecutableScript getImpTransformScript() + { + return impTransformScript; + } + + public void setImpTransformScript( ExecutableScript impTransformScript ) + { + this.impTransformScript = impTransformScript; + } + + @Basic + @Column( name = "preferred", nullable = false, columnDefinition = "BOOLEAN DEFAULT FALSE NOT NULL" ) + public boolean isPreferred() + { + return preferred; + } + + public void setPreferred( boolean preferred ) + { + this.preferred = preferred; + } + @JsonIgnore @Transient @Override diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerResourceRepository.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerResourceRepository.java index 260b80cd..d0562b06 100644 --- a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerResourceRepository.java +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerResourceRepository.java @@ -1,7 +1,7 @@ package org.dhis2.fhir.adapter.fhir.metadata.repository; /* - * Copyright (c) 2004-2018, University of Oslo + * Copyright (c) 2004-2019, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -59,11 +59,17 @@ public interface FhirServerResourceRepository extends JpaRepository, QuerydslPredicateExecutor, CustomFhirServerResourceRepository { @RestResource( exported = false ) - @Query( "SELECT r FROM #{#entityName} r JOIN FETCH r.fhirServer s WHERE s=:subscription AND r.fhirResourceType=:fhirResourceType ORDER BY r.id" ) + @Query( "SELECT r FROM #{#entityName} r JOIN FETCH r.fhirServer s WHERE s=:fhirServer AND r.fhirResourceType=:fhirResourceType ORDER BY r.id" ) @Nonnull - @Cacheable( key = "{#root.methodName, #a0.id, #a1}" ) + @Cacheable( key = "{#root.methodName, #a0.id, #a1}" ) Optional findFirstCached( - @Param( "subscription" ) @Nonnull FhirServer fhirServer, @Param( "fhirResourceType" ) @Nonnull FhirResourceType fhirResourceType ); + @Param( "fhirServer" ) @Nonnull FhirServer fhirServer, @Param( "fhirResourceType" ) @Nonnull FhirResourceType fhirResourceType ); + + @RestResource( exported = false ) + @Query( "SELECT r FROM #{#entityName} r JOIN FETCH r.fhirServer s WHERE s=:fhirServerId AND r.fhirResourceType=:fhirResourceType ORDER BY r.preferred DESC, r.id" ) + @Nonnull + @Cacheable( key = "{#root.methodName, #a0, #a1}" ) + Optional findFirstCached( @Param( "fhirServerId" ) @Nonnull UUID fhirServerId, @Param( "fhirResourceType" ) @Nonnull FhirResourceType fhirResourceType ); @Override @Nonnull diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerSystemRepository.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerSystemRepository.java index 44dfecc7..1efd50ff 100644 --- a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerSystemRepository.java +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerSystemRepository.java @@ -1,7 +1,7 @@ package org.dhis2.fhir.adapter.fhir.metadata.repository; /* - * Copyright (c) 2004-2018, University of Oslo + * Copyright (c) 2004-2019, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,6 +28,7 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import org.dhis2.fhir.adapter.fhir.metadata.model.FhirResourceType; import org.dhis2.fhir.adapter.fhir.metadata.model.FhirServer; import org.dhis2.fhir.adapter.fhir.metadata.model.FhirServerSystem; import org.springframework.cache.annotation.CacheConfig; @@ -45,6 +46,7 @@ import javax.annotation.Nonnull; import java.util.Collection; import java.util.List; +import java.util.Optional; import java.util.UUID; /** @@ -59,9 +61,15 @@ public interface FhirServerSystemRepository extends JpaRepository findByFhirServer( @Param( "subscription" ) @Nonnull FhirServer subscription ); + Collection findByFhirServer( @Param( "fhirServer" ) @Nonnull FhirServer fhirServer ); + + @RestResource( exported = false ) + @Nonnull + @Query( "SELECT rss FROM #{#entityName} rss WHERE rss.fhirServer=:fhirServer AND rss.fhirResourceType=:fhirResourceType" ) + @Cacheable( key = "{#root.methodName, #a0.id, #a1}" ) + Optional findOneByFhirServerResourceType( @Param( "fhirServer" ) @Nonnull FhirServer fhirServer, @Param( "fhirResourceType" ) @Nonnull FhirResourceType fhirResourceType ); @Override @Nonnull diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/repository/validator/BeforeCreateSaveFhirServerResourceValidator.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/repository/validator/BeforeCreateSaveFhirServerResourceValidator.java index c047050c..32f120be 100644 --- a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/repository/validator/BeforeCreateSaveFhirServerResourceValidator.java +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/metadata/repository/validator/BeforeCreateSaveFhirServerResourceValidator.java @@ -1,7 +1,7 @@ package org.dhis2.fhir.adapter.fhir.metadata.repository.validator; /* - * Copyright (c) 2004-2018, University of Oslo + * Copyright (c) 2004-2019, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -29,12 +29,18 @@ */ import org.apache.commons.lang3.StringUtils; +import org.dhis2.fhir.adapter.fhir.metadata.model.DataType; +import org.dhis2.fhir.adapter.fhir.metadata.model.ExecutableScript; +import org.dhis2.fhir.adapter.fhir.metadata.model.FhirResourceType; import org.dhis2.fhir.adapter.fhir.metadata.model.FhirServerResource; +import org.dhis2.fhir.adapter.fhir.metadata.model.ScriptType; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.Validator; +import reactor.util.annotation.NonNull; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** * Spring Data REST validator for {@link FhirServerResource}. @@ -63,10 +69,38 @@ public void validate( Object target, @Nonnull Errors errors ) { errors.rejectValue( "fhirResourceType", "FhirServerResource.fhirResourceType.null", "FHIR resource type is mandatory." ); } + else + { + checkValidTransformInScript( errors, "impTransformScript", fhirServerResource.getFhirResourceType(), fhirServerResource.getImpTransformScript() ); + } if ( StringUtils.length( fhirServerResource.getFhirCriteriaParameters() ) > FhirServerResource.MAX_CRITERIA_PARAMETERS_LENGTH ) { errors.rejectValue( "fhirCriteriaParameters", "FhirServerResource.fhirCriteriaParameters.length", new Object[]{ FhirServerResource.MAX_CRITERIA_PARAMETERS_LENGTH }, "FHIR criteria parameters must not be longer than {0} characters." ); } } + + protected static void checkValidTransformInScript( @NonNull Errors errors, @Nonnull String field, @Nonnull FhirResourceType fhirResourceType, @Nullable ExecutableScript executableScript ) + { + if ( executableScript == null ) + { + return; + } + if ( executableScript.getScript().getScriptType() != ScriptType.TRANSFORM_TO_DHIS ) + { + errors.rejectValue( field, "FhirServerResource." + field + ".scriptType", "Assigned script type for incoming transformation must be TRANSFORM_TO_DHIS." ); + } + if ( executableScript.getScript().getReturnType() != DataType.BOOLEAN ) + { + errors.rejectValue( field, "FhirServerResource." + field + ".returnType", "Assigned return type for incoming transformation script must be BOOLEAN." ); + } + if ( (executableScript.getScript().getInputType() != null) && (executableScript.getScript().getInputType().getFhirResourceType() != fhirResourceType) ) + { + errors.rejectValue( field, "FhirServerResource." + field + ".inputType", new Object[]{ fhirResourceType }, "Assigned input type for incoming transformation script must be the same as for the resource {0}." ); + } + if ( (executableScript.getScript().getOutputType() != null) && (executableScript.getScript().getOutputType().getFhirResourceType() != fhirResourceType) ) + { + errors.rejectValue( field, "FhirServerResource." + field + ".inputType", new Object[]{ fhirResourceType }, "Assigned input type for outgoing transformation script must be the same as for the resource {0}." ); + } + } } 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 f8cb7574..8c3de507 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 @@ -1,7 +1,7 @@ package org.dhis2.fhir.adapter.fhir.repository; /* - * Copyright (c) 2004-2018, University of Oslo + * Copyright (c) 2004-2019, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -52,6 +52,9 @@ public interface FhirResourceRepository @Nonnull Optional findRefreshed( @Nonnull UUID fhirServerId, @Nonnull FhirVersion fhirVersion, @Nonnull SubscriptionFhirEndpoint fhirEndpoint, @Nonnull String resourceType, @Nonnull String resourceId ); + @Nonnull + Optional findRefreshed( @Nonnull UUID fhirServerId, @Nonnull FhirVersion fhirVersion, @Nonnull SubscriptionFhirEndpoint fhirEndpoint, @Nonnull String resourceType, @Nonnull String resourceId, boolean transform ); + @Nonnull Optional find( @Nonnull UUID fhirServerId, @Nonnull FhirVersion fhirVersion, @Nonnull SubscriptionFhirEndpoint fhirEndpoint, @Nonnull String resourceType, @Nonnull String resourceId ); diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/AbstractFhirResourceRepositoryImpl.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/AbstractFhirResourceRepositoryImpl.java index 3307a91f..030b9012 100644 --- a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/AbstractFhirResourceRepositoryImpl.java +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/AbstractFhirResourceRepositoryImpl.java @@ -40,9 +40,12 @@ import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import org.dhis2.fhir.adapter.data.model.ProcessedItemInfo; +import org.dhis2.fhir.adapter.fhir.metadata.model.FhirResourceType; import org.dhis2.fhir.adapter.fhir.metadata.model.FhirServer; import org.dhis2.fhir.adapter.fhir.metadata.model.FhirServerResource; +import org.dhis2.fhir.adapter.fhir.metadata.model.ScriptVariable; import org.dhis2.fhir.adapter.fhir.metadata.model.SubscriptionFhirEndpoint; +import org.dhis2.fhir.adapter.fhir.metadata.repository.FhirServerResourceRepository; import org.dhis2.fhir.adapter.fhir.metadata.repository.event.AutoCreatedFhirServerResourceEvent; import org.dhis2.fhir.adapter.fhir.model.FhirVersion; import org.dhis2.fhir.adapter.fhir.model.SystemCodeValue; @@ -69,6 +72,7 @@ import javax.annotation.Nullable; import java.time.Instant; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -87,11 +91,14 @@ public abstract class AbstractFhirResourceRepositoryImpl implements FhirResource private final StoredFhirResourceService storedItemService; + private final FhirServerResourceRepository fhirServerResourceRepository; + private final Map fhirContexts; - public AbstractFhirResourceRepositoryImpl( @Nonnull StoredFhirResourceService storedItemService, @Nonnull ObjectProvider> fhirContexts ) + public AbstractFhirResourceRepositoryImpl( @Nonnull StoredFhirResourceService storedItemService, @Nonnull FhirServerResourceRepository fhirServerResourceRepository, @Nonnull ObjectProvider> fhirContexts ) { this.storedItemService = storedItemService; + this.fhirServerResourceRepository = fhirServerResourceRepository; this.fhirContexts = fhirContexts.getIfAvailable( Collections::emptyList ).stream().filter( fc -> (FhirVersion.get( fc.getVersion().getVersion() ) != null) ) .collect( Collectors.toMap( fc -> FhirVersion.get( fc.getVersion().getVersion() ), fc -> fc ) ); } @@ -107,10 +114,10 @@ public Optional findFhirContext( @Nonnull FhirVersion fhirVersion ) } @HystrixCommand - @CachePut( key = "{#fhirServerId, #fhirVersion, #resourceType, #resourceId}", unless = "#result==null" ) + @CachePut( key = "{#fhirServerId, #fhirVersion, #resourceType, #resourceId, #transform}", unless = "#result==null" ) @Nonnull @Override - public Optional findRefreshed( @Nonnull UUID fhirServerId, @Nonnull FhirVersion fhirVersion, @Nonnull SubscriptionFhirEndpoint fhirEndpoint, @Nonnull String resourceType, @Nonnull String resourceId ) + public Optional findRefreshed( @Nonnull UUID fhirServerId, @Nonnull FhirVersion fhirVersion, @Nonnull SubscriptionFhirEndpoint fhirEndpoint, @Nonnull String resourceType, @Nonnull String resourceId, boolean transform ) { final FhirContext fhirContext = fhirContexts.get( fhirVersion ); final IGenericClient client = FhirClientUtils.createClient( fhirContext, fhirEndpoint ); @@ -130,7 +137,16 @@ public Optional findRefreshed( @Nonnull UUID fhirServerId, @Nonnu } @HystrixCommand - @Cacheable( key = "{#fhirServerId, #fhirVersion, #resourceType, #resourceId}", unless = "#result==null" ) + @CachePut( key = "{#fhirServerId, #fhirVersion, #resourceType, #resourceId, true}", unless = "#result==null" ) + @Nonnull + @Override + public Optional findRefreshed( @Nonnull UUID fhirServerId, @Nonnull FhirVersion fhirVersion, @Nonnull SubscriptionFhirEndpoint fhirEndpoint, @Nonnull String resourceType, @Nonnull String resourceId ) + { + return findRefreshed( fhirServerId, fhirVersion, fhirEndpoint, resourceType, resourceId, true ); + } + + @HystrixCommand + @Cacheable( key = "{#fhirServerId, #fhirVersion, #resourceType, #resourceId, true}", unless = "#result==null" ) @Nonnull @Override public Optional find( @Nonnull UUID fhirServerId, @Nonnull FhirVersion fhirVersion, @Nonnull SubscriptionFhirEndpoint fhirEndpoint, @Nonnull String resourceType, @Nonnull String resourceId ) @@ -175,13 +191,13 @@ public Optional findByCode( @Nonnull UUID fhirServerId, @Nonnull } @HystrixCommand - @CacheEvict( key = "{#subscription.id, #subscription.fhirVersion, " + - "T(org.dhis2.fhir.adapter.fhir.metadata.model.FhirResourceType).getByResource(#resource).getResourceTypeName(), #resource.getIdElement().getIdPart()}" ) + @CacheEvict( key = "{#fhirServer.id, #fhirServer.fhirVersion, " + + "T(org.dhis2.fhir.adapter.fhir.metadata.model.FhirResourceType).getByResource(#resource).getResourceTypeName(), #resource.getIdElement().getIdPart(), true}" ) @Override - public boolean delete( @Nonnull FhirServer subscription, @Nonnull IBaseResource resource ) + public boolean delete( @Nonnull FhirServer fhirServer, @Nonnull IBaseResource resource ) { - final FhirContext fhirContext = fhirContexts.get( subscription.getFhirVersion() ); - final IGenericClient client = FhirClientUtils.createClient( fhirContext, subscription.getFhirEndpoint() ); + final FhirContext fhirContext = fhirContexts.get( fhirServer.getFhirVersion() ); + final IGenericClient client = FhirClientUtils.createClient( fhirContext, fhirServer.getFhirEndpoint() ); try { @@ -201,14 +217,14 @@ public boolean delete( @Nonnull FhirServer subscription, @Nonnull IBaseResource } @HystrixCommand - @CacheEvict( key = "{#subscription.id, #subscription.fhirVersion, " + - "T(org.dhis2.fhir.adapter.fhir.metadata.model.FhirResourceType).getByResource(#resource).getResourceTypeName(), #resource.getIdElement().getIdPart()}" ) + @CacheEvict( key = "{#fhirServer.id, #fhirServer.fhirVersion, " + + "T(org.dhis2.fhir.adapter.fhir.metadata.model.FhirResourceType).getByResource(#resource).getResourceTypeName(), #resource.getIdElement().getIdPart(), true}" ) @Nonnull @Override - public IBaseResource save( @Nonnull FhirServer subscription, @Nonnull IBaseResource resource ) + public IBaseResource save( @Nonnull FhirServer fhirServer, @Nonnull IBaseResource resource ) { - final FhirContext fhirContext = fhirContexts.get( subscription.getFhirVersion() ); - final IGenericClient client = FhirClientUtils.createClient( fhirContext, subscription.getFhirEndpoint() ); + final FhirContext fhirContext = fhirContexts.get( fhirServer.getFhirVersion() ); + final IGenericClient client = FhirClientUtils.createClient( fhirContext, fhirServer.getFhirEndpoint() ); final MethodOutcome methodOutcome; if ( resource.getIdElement().hasIdPart() ) @@ -242,11 +258,11 @@ else if ( (methodOutcome.getId() != null) && methodOutcome.getId().hasVersionIdP if ( processedItemInfo == null ) { logger.info( "FHIR server {} does neither return complete resource with update timestamp nor a version. " + - "Duplicate detection for resource {} will not work.", subscription.getId(), methodOutcome.getId() ); + "Duplicate detection for resource {} will not work.", fhirServer.getId(), methodOutcome.getId() ); } else { - storedItemService.stored( subscription, processedItemInfo.toIdString( Instant.now() ) ); + storedItemService.stored( fhirServer, processedItemInfo.toIdString( Instant.now() ) ); } final IBaseResource result; @@ -322,4 +338,23 @@ protected Optional findByToken( @Nonnull UUID fhirServerId, @Nonn logger.debug( "Read {}?{}={} from FHIR endpoints {} (found={}).", resourceType, identifier, field, fhirEndpoint.getBaseUrl(), (resource != null) ); return Optional.ofNullable( resource ); } + + @Nonnull + protected Optional transform( @Nonnull UUID fhirServerId, @Nonnull FhirVersion fhirVersion, @Nullable IBaseResource resource ) + { + final FhirResourceType fhirResourceType = FhirResourceType.getByResource( resource ); + if ( fhirResourceType == null ) + { + return Optional.empty(); + } + final Optional fhirServerResource = fhirServerResourceRepository.findFirstCached( fhirServerId, fhirResourceType ); + if ( fhirServerResource.isPresent() && (fhirServerResource.get().getImpTransformScript() != null) ) + { + final Map variables = new HashMap<>(); + variables.put( ScriptVariable.INPUT.getVariableName(), resource ); + variables.put( ScriptVariable.OUTPUT.getVariableName(), resource ); + + } + return Optional.of( resource ); + } } diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/AbstractRepositoryResourceUtils.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/AbstractRepositoryResourceUtils.java new file mode 100644 index 00000000..9fa5c9f7 --- /dev/null +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/repository/impl/AbstractRepositoryResourceUtils.java @@ -0,0 +1,64 @@ +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 org.dhis2.fhir.adapter.fhir.metadata.model.FhirServer; +import org.dhis2.fhir.adapter.fhir.metadata.repository.FhirServerSystemRepository; +import org.dhis2.fhir.adapter.fhir.transform.fhir.model.ResourceSystem; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Abstract class with repository resource utils. + * + * @author volsch + */ +public abstract class AbstractRepositoryResourceUtils +{ + private final FhirServer fhirServer; + + private final FhirServerSystemRepository fhirServerSystemRepository; + + public AbstractRepositoryResourceUtils( @Nonnull FhirServer fhirServer, @Nonnull FhirServerSystemRepository fhirServerSystemRepository ) + { + this.fhirServer = fhirServer; + this.fhirServerSystemRepository = fhirServerSystemRepository; + } + + @Nonnull + public abstract IBaseResource createResource( @Nonnull Object resourceType ); + + @Nullable + public ResourceSystem getResourceSystem( @Nullable Object resourceType ) + { + return null; + } +} diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/server/AbstractFhirServerController.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/server/AbstractFhirServerController.java new file mode 100644 index 00000000..4e607f6c --- /dev/null +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/server/AbstractFhirServerController.java @@ -0,0 +1,144 @@ +package org.dhis2.fhir.adapter.fhir.server; + +/* + * 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 org.apache.commons.lang3.StringUtils; +import org.dhis2.fhir.adapter.fhir.metadata.model.FhirServerResource; +import org.dhis2.fhir.adapter.fhir.metadata.repository.FhirServerResourceRepository; +import org.dhis2.fhir.adapter.rest.RestResourceNotFoundException; +import org.dhis2.fhir.adapter.rest.RestUnauthorizedException; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +/** + * Abstract base class for controllers that receive FHIR resources or notifications + * about FHIR resources from FHIR servers. + * + * @author volsch + */ +public abstract class AbstractFhirServerController +{ + private final FhirServerResourceRepository resourceRepository; + + private final FhirServerRestHookProcessor processor; + + protected AbstractFhirServerController( @Nonnull FhirServerResourceRepository resourceRepository, @Nonnull FhirServerRestHookProcessor processor ) + { + this.resourceRepository = resourceRepository; + this.processor = processor; + } + + @Nonnull + public FhirServerResourceRepository getResourceRepository() + { + return resourceRepository; + } + + @Nonnull + protected FhirServerRestHookProcessor getProcessor() + { + return processor; + } + + @Nonnull + protected ResponseEntity processPayload( @Nonnull FhirServerResource fhirServerResource, + @Nonnull String resourceType, @Nonnull String resourceId, HttpEntity requestEntity ) + { + if ( (requestEntity.getBody() == null) || (requestEntity.getBody().length == 0) ) + { + return createBadRequestResponse( "Payload expected." ); + } + + final MediaType mediaType = requestEntity.getHeaders().getContentType(); + final String fhirResource = new String( requestEntity.getBody(), getCharset( mediaType ) ); + processPayload( fhirServerResource, (mediaType == null) ? null : mediaType.toString(), resourceType, resourceId, fhirResource ); + return new ResponseEntity<>( HttpStatus.OK ); + } + + protected void processPayload( @Nonnull FhirServerResource fhirServerResource, @Nullable String contentType, + @Nonnull String resourceType, @Nonnull String resourceId, @Nonnull String fhirResource ) + { + processor.process( fhirServerResource, contentType, resourceType, resourceId, fhirResource ); + } + + @Nonnull + protected Charset getCharset( @Nullable MediaType contentType ) + { + Charset charset; + if ( contentType == null ) + { + charset = StandardCharsets.UTF_8; + } + else + { + charset = contentType.getCharset(); + if ( charset == null ) + { + charset = StandardCharsets.UTF_8; + } + } + return charset; + } + + @Nonnull + protected ResponseEntity createBadRequestResponse( @Nonnull String message ) + { + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType( MediaType.TEXT_PLAIN ); + return new ResponseEntity<>( message.getBytes( StandardCharsets.UTF_8 ), headers, HttpStatus.BAD_REQUEST ); + } + + protected void validateRequest( @Nonnull UUID fhirServerId, FhirServerResource fhirServerResource, String authorization ) + { + if ( !fhirServerResource.getFhirServer().getId().equals( fhirServerId ) ) + { + // do not give detail if the resource or the subscription cannot be found + throw new RestResourceNotFoundException( "FHIR server data for resource cannot be found: " + fhirServerResource ); + } + if ( fhirServerResource.isExpOnly() ) + { + throw new RestResourceNotFoundException( "FHIR server resource is intended for export only: " + fhirServerResource ); + } + + if ( StringUtils.isNotBlank( fhirServerResource.getFhirServer().getAdapterEndpoint().getAuthorizationHeader() ) && + !fhirServerResource.getFhirServer().getAdapterEndpoint().getAuthorizationHeader().equals( authorization ) ) + { + throw new RestUnauthorizedException( "Authentication has failed." ); + } + } +} diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/server/FhirServerRestHookController.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/server/FhirServerRestHookController.java index c9a7bd52..17048441 100644 --- a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/server/FhirServerRestHookController.java +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/server/FhirServerRestHookController.java @@ -28,17 +28,12 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import org.apache.commons.lang3.StringUtils; import org.dhis2.fhir.adapter.fhir.metadata.model.FhirServerResource; import org.dhis2.fhir.adapter.fhir.metadata.repository.FhirServerResourceRepository; import org.dhis2.fhir.adapter.rest.RestResourceNotFoundException; -import org.dhis2.fhir.adapter.rest.RestUnauthorizedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -48,9 +43,6 @@ import org.springframework.web.bind.annotation.RestController; import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; import java.util.UUID; /** @@ -64,18 +56,13 @@ */ @RestController @RequestMapping( "/remote-fhir-rest-hook" ) -public class FhirServerRestHookController +public class FhirServerRestHookController extends AbstractFhirServerController { private final Logger logger = LoggerFactory.getLogger( getClass() ); - private final FhirServerResourceRepository resourceRepository; - - private final FhirServerRestHookProcessor processor; - public FhirServerRestHookController( @Nonnull FhirServerResourceRepository resourceRepository, @Nonnull FhirServerRestHookProcessor processor ) { - this.resourceRepository = resourceRepository; - this.processor = processor; + super( resourceRepository, processor ); } @RequestMapping( path = "/{fhirServerId}/{fhirServerResourceId}/{resourceType}/{resourceId}/**", method = { RequestMethod.POST, RequestMethod.PUT } ) @@ -95,18 +82,8 @@ public ResponseEntity receiveWithPayload( @RequestHeader( value = "Authorization", required = false ) String authorization, @Nonnull HttpEntity requestEntity ) { - if ( (requestEntity.getBody() == null) || (requestEntity.getBody().length == 0) ) - { - return createBadRequestResponse( "Payload expected." ); - } - - final FhirServerResource fhirServerResource = lookupFhirServerResource( fhirServerId, fhirServerResourceId, authorization ); - final MediaType mediaType = requestEntity.getHeaders().getContentType(); - final String fhirResource = new String( requestEntity.getBody(), getCharset( mediaType ) ); - processor.process( fhirServerResource, (mediaType == null) ? null : mediaType.toString(), - resourceType, resourceId, fhirResource ); - - return new ResponseEntity<>( HttpStatus.OK ); + return processPayload( lookupFhirServerResource( fhirServerId, fhirServerResourceId, authorization ), + resourceType, resourceId, requestEntity ); } @PostMapping( path = "/{fhirServerId}/{fhirServerResourceId}" ) @@ -114,57 +91,15 @@ public void receive( @PathVariable UUID fhirServerId, @PathVariable UUID fhirSer @RequestHeader( value = "Authorization", required = false ) String authorization ) { final FhirServerResource fhirServerResource = lookupFhirServerResource( fhirServerId, fhirServerResourceId, authorization ); - processor.process( fhirServerResource ); + getProcessor().process( fhirServerResource ); } @Nonnull protected FhirServerResource lookupFhirServerResource( @Nonnull UUID fhirServerId, @Nonnull UUID fhirServerResourceId, String authorization ) { - final FhirServerResource fhirServerResource = resourceRepository.findOneByIdCached( fhirServerResourceId ) + final FhirServerResource fhirServerResource = getResourceRepository().findOneByIdCached( fhirServerResourceId ) .orElseThrow( () -> new RestResourceNotFoundException( "FHIR server data for resource cannot be found: " + fhirServerResourceId ) ); - if ( !fhirServerResource.getFhirServer().getId().equals( fhirServerId ) ) - { - // do not give detail if the resource or the subscription cannot be found - throw new RestResourceNotFoundException( "FHIR server data for resource cannot be found: " + fhirServerResourceId ); - } - if ( fhirServerResource.isExpOnly() ) - { - throw new RestResourceNotFoundException( "FHIR server resource is intended for export only: " + fhirServerResourceId ); - } - - if ( StringUtils.isNotBlank( fhirServerResource.getFhirServer().getAdapterEndpoint().getAuthorizationHeader() ) && - !fhirServerResource.getFhirServer().getAdapterEndpoint().getAuthorizationHeader().equals( authorization ) ) - { - throw new RestUnauthorizedException( "Authentication has failed." ); - } + validateRequest( fhirServerId, fhirServerResource, authorization ); return fhirServerResource; } - - @Nonnull - private Charset getCharset( @Nullable MediaType contentType ) - { - Charset charset; - if ( contentType == null ) - { - charset = StandardCharsets.UTF_8; - } - else - { - charset = contentType.getCharset(); - if ( charset == null ) - { - charset = StandardCharsets.UTF_8; - } - } - return charset; - } - - @Nonnull - private ResponseEntity createBadRequestResponse( @Nonnull String message ) - { - final HttpHeaders headers = new HttpHeaders(); - headers.add( HttpHeaders.CONTENT_TYPE, "text/plain; charset=UTF-8" ); - return new ResponseEntity<>( - message.getBytes( StandardCharsets.UTF_8 ), headers, HttpStatus.BAD_REQUEST ); - } } diff --git a/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/server/FhirServerSyncController.java b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/server/FhirServerSyncController.java new file mode 100644 index 00000000..c2cf3ea8 --- /dev/null +++ b/fhir/src/main/java/org/dhis2/fhir/adapter/fhir/server/FhirServerSyncController.java @@ -0,0 +1,158 @@ +package org.dhis2.fhir.adapter.fhir.server; + +/* + * 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 org.dhis2.fhir.adapter.fhir.metadata.model.FhirResourceType; +import org.dhis2.fhir.adapter.fhir.metadata.model.FhirServerResource; +import org.dhis2.fhir.adapter.fhir.metadata.repository.FhirServerResourceRepository; +import org.dhis2.fhir.adapter.fhir.model.FhirVersion; +import org.dhis2.fhir.adapter.fhir.repository.FhirResourceRepository; +import org.dhis2.fhir.adapter.rest.RestResourceNotFoundException; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +/** + * Accepts resources that are initiated by a remote FHIR server + * synchronization (e.g. OpenMRS). + * + * @author volsch + */ +@RestController +@RequestMapping( "/remote-fhir-sync" ) +public class FhirServerSyncController extends AbstractFhirServerController +{ + private final Logger logger = LoggerFactory.getLogger( getClass() ); + + public static final MediaType FHIR_JSON_MEDIA_TYPE = MediaType.parseMediaType( "application/fhir+json;charset=UTF-8" ); + + private final FhirResourceRepository fhirResourceRepository; + + public FhirServerSyncController( @Nonnull FhirServerResourceRepository resourceRepository, @Nonnull FhirServerRestHookProcessor processor, + @Nonnull FhirResourceRepository fhirResourceRepository ) + { + super( resourceRepository, processor ); + this.fhirResourceRepository = fhirResourceRepository; + } + + @RequestMapping( path = "/{fhirServerId}/{resourceType}/{resourceId}", method = RequestMethod.DELETE ) + public ResponseEntity delete( + @PathVariable( "fhirServerId" ) UUID fhirServerId, @PathVariable( "resourceType" ) String resourceType, @PathVariable( "resourceId" ) String resourceId, + @RequestHeader( value = "Authorization", required = false ) String authorization ) + { + final FhirResourceType fhirResourceType = FhirResourceType.getByResourceTypeName( resourceType ); + if ( fhirResourceType == null ) + { + return createBadRequestResponse( "Unknown resource type: " + resourceType ); + } + + lookupFhirServerResource( fhirServerId, fhirResourceType, authorization ); + // not yet supported + return new ResponseEntity<>( HttpStatus.NO_CONTENT ); + } + + @RequestMapping( path = "/{fhirServerId}/{resourceType}/{resourceId}", method = RequestMethod.GET ) + public ResponseEntity getRemoteResource( @PathVariable( "fhirServerId" ) UUID fhirServerId, + @PathVariable( "resourceType" ) String resourceType, @PathVariable( "resourceId" ) String resourceId, + @RequestHeader( value = "Authorization", required = false ) String authorization ) + { + final FhirResourceType fhirResourceType = FhirResourceType.getByResourceTypeName( resourceType ); + if ( fhirResourceType == null ) + { + return createBadRequestResponse( "Unknown resource type: " + resourceType ); + } + + final FhirServerResource fhirServerResource = lookupFhirServerResource( fhirServerId, fhirResourceType, authorization ); + if ( !fhirServerResource.getFhirServer().isRemoteSyncEnabled() ) + { + return new ResponseEntity<>( HttpStatus.FORBIDDEN ); + } + + // read resource from remote server, process payload as notification, return resource again + final FhirVersion fhirVersion = fhirServerResource.getFhirServer().getFhirVersion(); + final Optional resource = fhirResourceRepository.findRefreshed( fhirServerResource.getFhirServer().getId(), + fhirVersion, fhirServerResource.getFhirServer().getFhirEndpoint(), resourceType, resourceId ); + if ( !resource.isPresent() ) + { + return new ResponseEntity<>( HttpStatus.NOT_FOUND ); + } + + final FhirContext fhirContext = fhirResourceRepository.findFhirContext( fhirVersion ) + .orElseThrow( () -> new IllegalStateException( "FHIR context for FHIR version " + fhirVersion + " has not been defined." ) ); + final String fhirResource = fhirContext.newJsonParser().encodeResourceToString( resource.get() ); + processPayload( fhirServerResource, FHIR_JSON_MEDIA_TYPE.toString(), resourceType, resourceId, fhirResource ); + + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType( FHIR_JSON_MEDIA_TYPE ); + headers.setCacheControl( CacheControl.noCache() ); + return new ResponseEntity<>( fhirResource.getBytes( Objects.requireNonNull( FHIR_JSON_MEDIA_TYPE.getCharset() ) ), headers, HttpStatus.OK ); + } + + @RequestMapping( path = "/{fhirServerId}/{resourceType}/{resourceId}", method = { RequestMethod.POST, RequestMethod.PUT } ) + public ResponseEntity receiveWithPayload( + @PathVariable( "fhirServerId" ) UUID fhirServerId, @PathVariable( "resourceType" ) String resourceType, @PathVariable( "resourceId" ) String resourceId, + @RequestHeader( value = "Authorization", required = false ) String authorization, @Nonnull HttpEntity requestEntity ) + { + final FhirResourceType fhirResourceType = FhirResourceType.getByResourceTypeName( resourceType ); + if ( fhirResourceType == null ) + { + return createBadRequestResponse( "Unknown resource type: " + resourceType ); + } + + final FhirServerResource fhirServerResource = lookupFhirServerResource( fhirServerId, fhirResourceType, authorization ); + return processPayload( fhirServerResource, resourceType, resourceId, requestEntity ); + } + + @Nonnull + protected FhirServerResource lookupFhirServerResource( @Nonnull UUID fhirServerId, @Nonnull FhirResourceType fhirResourceType, @Nullable String authorization ) + { + final FhirServerResource fhirServerResource = getResourceRepository().findFirstCached( fhirServerId, fhirResourceType ) + .orElseThrow( () -> new RestResourceNotFoundException( "FHIR server data for resource " + fhirResourceType + " of FHIR server " + fhirServerId + " cannot be found." ) ); + validateRequest( fhirServerId, fhirServerResource, authorization ); + return fhirServerResource; + } +} diff --git a/fhir/src/main/resources/db/migration/openmrs/V1.1.0.6_21_0__OpenMRS_Advanced_Subscription.sql b/fhir/src/main/resources/db/migration/openmrs/V1.1.0.6_21_0__OpenMRS_Advanced_Subscription.sql new file mode 100644 index 00000000..ac197290 --- /dev/null +++ b/fhir/src/main/resources/db/migration/openmrs/V1.1.0.6_21_0__OpenMRS_Advanced_Subscription.sql @@ -0,0 +1,34 @@ +/* + * 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 PROGRAM_STAGE_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. + */ + +-- @formatter:off + +-- FHIR Adapter Subscription User: openmrs Password: openmrs +UPDATE fhir_server SET remote_base_url='http://localhost:8090/openmrs/ws/fhir', web_hook_authorization_header='Basic b3Blbm1yczpvcGVubXJz', remote_sync_enabled=TRUE WHERE id='73cd99c5-0ca8-42ad-a53b-1891fccce08f'; +-- OpenMRS User: admin Password: Admin123 +UPDATE fhir_server_header SET value='Basic YWRtaW46QWRtaW4xMjM=' WHERE name='Authorization' AND fhir_server_id ='73cd99c5-0ca8-42ad-a53b-1891fccce08f'; diff --git a/fhir/src/main/resources/db/migration/production/V1.1.0.6_0_0__Advanced_Subscription.sql b/fhir/src/main/resources/db/migration/production/V1.1.0.6_0_0__Advanced_Subscription.sql index 6c2c1c8f..42c0c09c 100644 --- a/fhir/src/main/resources/db/migration/production/V1.1.0.6_0_0__Advanced_Subscription.sql +++ b/fhir/src/main/resources/db/migration/production/V1.1.0.6_0_0__Advanced_Subscription.sql @@ -42,3 +42,14 @@ CREATE TABLE fhir_subscription_resource( CONSTRAINT fhir_subscription_resource_uk1 UNIQUE(fhir_server_resource_id, fhir_resource_id) ); CREATE INDEX fhir_subscription_resource_i1 ON fhir_subscription_resource(fhir_server_resource_id, created_at); + +ALTER TABLE fhir_server ADD remote_sync_enabled BOOLEAN DEFAULT FALSE NOT NULL; +COMMENT ON COLUMN fhir_server.remote_sync_enabled IS 'Specifies if a FHIR notification can be simulated by a remote FHIR server by invoking a read request on a FHIR resource.'; + +ALTER TABLE fhir_server_resource + ADD COLUMN imp_transform_script_id UUID, + ADD COLUMN preferred BOOLEAN DEFAULT FALSE NOT NULL, + ADD CONSTRAINT fhir_server_resource_fk3 FOREIGN KEY (imp_transform_script_id) REFERENCES fhir_executable_script(id); +COMMENT ON COLUMN fhir_server_resource.imp_transform_script_id IS 'Executable transformation script that transform incoming FHIR resources.'; +COMMENT ON COLUMN fhir_server_resource.preferred IS 'Specifies if this resource definition is the preferred resource definition for the resource type when no resource definition can be determined otherwise.'; +CREATE INDEX fhir_server_resource_i2 ON fhir_server_resource(imp_transform_script_id); diff --git a/fhir/src/test/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerRepositoryRestDocsTest.java b/fhir/src/test/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerRepositoryRestDocsTest.java index 353d0a87..3177959d 100644 --- a/fhir/src/test/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerRepositoryRestDocsTest.java +++ b/fhir/src/test/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerRepositoryRestDocsTest.java @@ -1,7 +1,7 @@ package org.dhis2.fhir.adapter.fhir.metadata.repository; /* - * Copyright (c) 2004-2018, University of Oslo + * Copyright (c) 2004-2019, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -88,6 +88,8 @@ public void createFhirServer() throws Exception fields.withPath( "toleranceMillis" ).description( "The number of milli-seconds to subtract from the last updated timestamp when searching for created and updated resources." ).type( JsonFieldType.NUMBER ), fields.withPath( "autoCreatedSubscriptionResources" ).description( "Subscription resources for which the subscriptions should be created automatically when creating the subscription resource. This value will not be returned and can only " + "be used when creating and updating the entity." ).type( JsonFieldType.ARRAY ).optional(), + fields.withPath( "remoteSyncEnabled" ).description( "Specifies if a FHIR notification can be simulated by a remote FHIR server by invoking a read request on a FHIR resource." ) + .type( JsonFieldType.BOOLEAN ).optional(), fields.withPath( "adapterEndpoint" ).description( "Specifies FHIR server settings that are relevant for the adapter." ).type( JsonFieldType.OBJECT ), fields.withPath( "adapterEndpoint.baseUrl" ).description( "The base URL of the adapter that is used to register the subscription on the FHIR service. " + "If the FHIR service runs on a different server, the URL must not contain localhost. If this URL is not specified it is calculated automatically." ).type( JsonFieldType.STRING ).optional(), @@ -166,6 +168,8 @@ public void readFhirServer() throws Exception fields.withPath( "toleranceMillis" ).description( "The number of milli-seconds to subtract from the last updated timestamp when searching for created and updated resources." ).type( JsonFieldType.NUMBER ), fields.withPath( "autoCreatedSubscriptionResources" ).description( "Subscription resources for which the subscriptions should be created automatically when creating the subscription resource. This value will not be returned and can only " + "be used when creating and updating the entity." ).type( JsonFieldType.ARRAY ).optional(), + fields.withPath( "remoteSyncEnabled" ).description( "Specifies if a FHIR notification can be simulated by a remote FHIR server by invoking a read request on a FHIR resource." ) + .type( JsonFieldType.BOOLEAN ).optional(), fields.withPath( "adapterEndpoint" ).description( "Specifies FHIR server settings that are relevant for the adapter." ).type( JsonFieldType.OBJECT ), fields.withPath( "adapterEndpoint.baseUrl" ).description( "The base URL of the adapter that is used to register the subscription on the FHIR service. " + "If the FHIR service runs on a different server, the URL must not contain localhost. If this URL is not specified it is calculated automatically." ).type( JsonFieldType.STRING ).optional(), diff --git a/fhir/src/test/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerResourceRepositoryRestDocsTest.java b/fhir/src/test/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerResourceRepositoryRestDocsTest.java index 99e1c7c9..b6462aa7 100644 --- a/fhir/src/test/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerResourceRepositoryRestDocsTest.java +++ b/fhir/src/test/java/org/dhis2/fhir/adapter/fhir/metadata/repository/FhirServerResourceRepositoryRestDocsTest.java @@ -1,7 +1,7 @@ package org.dhis2.fhir.adapter.fhir.metadata.repository; /* - * Copyright (c) 2004-2018, University of Oslo + * Copyright (c) 2004-2019, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -89,7 +89,11 @@ public void createFhirServerResource() throws Exception fields.withPath( "description" ).description( "The detailed description of the purpose of the subscribed FHIR resource." ).type( JsonFieldType.STRING ).optional(), fields.withPath( "fhirCriteriaParameters" ).description( "The prefix that should be added to the codes when mapping them to DHIS2." ).type( JsonFieldType.STRING ).optional(), fields.withPath( "expOnly" ).description( "Specifies that this is only used for exporting FHIR resources. Subscription requests are not accepted." ) - .type( JsonFieldType.BOOLEAN ).optional() + .type( JsonFieldType.BOOLEAN ).optional(), + fields.withPath( "preferred" ).description( "Specifies if this resource definition is the preferred resource definition for the resource type when no resource definition can be determined otherwise." ) + .type( JsonFieldType.BOOLEAN ).optional(), + fields.withPath( "impTransformScript" ).description( "Link to the executable transformation script that transform incoming FHIR resources." ) + .type( JsonFieldType.STRING ).optional() ) ) ).andReturn().getResponse().getHeader( "Location" ); mockMvc @@ -114,7 +118,8 @@ public void readFhirServerResource() throws Exception .andDo( documentationHandler.document( links( linkWithRel( "self" ).description( "Link to this resource itself." ), linkWithRel( "fhirServerResource" ).description( "Link to this resource itself." ), - linkWithRel( "fhirServer" ).description( "The reference to the FHIR server to which this resource belongs to." ) ), responseFields( + linkWithRel( "fhirServer" ).description( "The reference to the FHIR server to which this resource belongs to." ), + linkWithRel( "impTransformScript" ).description( "Link to the executable transformation script that transform incoming FHIR resources." ).optional() ), responseFields( attributes( key( "title" ).value( "Fields for FHIR server resource reading" ) ), fields.withPath( "createdAt" ).description( "The timestamp when the resource has been created." ).type( JsonFieldType.STRING ), fields.withPath( "lastUpdatedBy" ).description( "The ID of the user that has updated the user the last time or null if the data has been imported to the database directly." ).type( JsonFieldType.STRING ).optional(), @@ -126,6 +131,8 @@ public void readFhirServerResource() throws Exception .type( JsonFieldType.BOOLEAN ).optional(), fields.withPath( "virtual" ).description( "Specifies that there is no subscription for this FHIR resource since the FHIR service may not accept subscription for this resource type (just available as contained resources)." ) .type( JsonFieldType.BOOLEAN ).optional(), + fields.withPath( "preferred" ).description( "Specifies if this resource definition is the preferred resource definition for the resource type when no resource definition can be determined otherwise." ) + .type( JsonFieldType.BOOLEAN ).optional(), fields.withPath( "hirSubscriptionId" ).description( "The ID of the automatically created FHIR subscription on the FHIR service." ).type( JsonFieldType.STRING ).optional(), subsectionWithPath( "_links" ).description( "Links to other resources" ) ) ) ); diff --git a/pom.xml b/pom.xml index ad3bc50b..9db614bb 100644 --- a/pom.xml +++ b/pom.xml @@ -274,6 +274,12 @@ classpath:db/migration/production,classpath:db/migration/programs,classpath:db/migration/sample + + openmrs + + classpath:db/migration/production,classpath:db/migration/programs,classpath:db/migration/sample,classpath:db/migration/openmrs + + executable-war