diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index 05899236b3f..f070265cf2f 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index a1e23f8009a..d3ada347401 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 983c369ed48..7c7d7315aab 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index 4743c0ad975..3df47882fc3 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -4,7 +4,7 @@ 4.0.0 ca.uhn.hapi.fhir hapi-fhir-bom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT pom HAPI FHIR BOM @@ -12,7 +12,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-checkstyle/pom.xml b/hapi-fhir-checkstyle/pom.xml index 6a0b56429a1..31bbfcee48f 100644 --- a/hapi-fhir-checkstyle/pom.xml +++ b/hapi-fhir-checkstyle/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml index 1c481b90a67..b8882614950 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml index 55fa8cdd466..e6450ae7a66 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-app/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir-cli - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index 7f9def2878b..7e9c2d93ad6 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index a668e5797ad..1a7e9853776 100644 --- a/hapi-fhir-client-okhttp/pom.xml +++ b/hapi-fhir-client-okhttp/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index fbc96865d68..b021a603bd3 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index a27edf28308..b43ceaf284c 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index eb875b3c989..130765ef487 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index 0c93e3a5496..4a6008791cf 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/upgrade.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/upgrade.md index e69de29bb2d..28f6fced45f 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/upgrade.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/upgrade.md @@ -0,0 +1,9 @@ +This release introduces significant a change to the mechanism performing submission of resource modification events +to the message broker. Previously, an event would be submitted as part of the synchronous transaction +modifying a resource. Synchronous submission yielded responsive publishing with the caveat that events would be dropped +upon submission failure. + +We have replaced the synchronous mechanism with a two stage process. Events are initially stored in +database upon completion of the transaction and subsequently submitted to the broker by a scheduled task. +This new asynchronous submission mechanism will introduce a slight delay in event publishing. It is our view that such +delay is largely compensated by the capability to retry submission upon failure which will eliminate event losses. diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index 1ba4b1dac5e..ac09fce6f88 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index 26799723eaf..90c9ff67494 100644 --- a/hapi-fhir-jaxrsserver-base/pom.xml +++ b/hapi-fhir-jaxrsserver-base/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpa/pom.xml b/hapi-fhir-jpa/pom.xml index fd06d0e50f6..b88a5b25755 100644 --- a/hapi-fhir-jpa/pom.xml +++ b/hapi-fhir-jpa/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index e27538007c3..d74dac6eef2 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java index 941f2273b7c..ce5fee55750 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/JpaConfig.java @@ -57,6 +57,7 @@ import ca.uhn.fhir.jpa.dao.MatchResourceUrlService; import ca.uhn.fhir.jpa.dao.ObservationLastNIndexPersistSvc; import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; import ca.uhn.fhir.jpa.dao.TransactionProcessor; +import ca.uhn.fhir.jpa.dao.data.IResourceModifiedDao; import ca.uhn.fhir.jpa.dao.data.IResourceSearchUrlDao; import ca.uhn.fhir.jpa.dao.expunge.ExpungeEverythingService; import ca.uhn.fhir.jpa.dao.expunge.ExpungeOperation; @@ -155,6 +156,7 @@ import ca.uhn.fhir.jpa.searchparam.extractor.IResourceLinkResolver; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc; import ca.uhn.fhir.jpa.sp.SearchParamPresenceSvcImpl; +import ca.uhn.fhir.jpa.subscription.ResourceModifiedMessagePersistenceSvcImpl; import ca.uhn.fhir.jpa.term.TermCodeSystemStorageSvcImpl; import ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl; import ca.uhn.fhir.jpa.term.TermReadSvcImpl; @@ -181,6 +183,7 @@ import ca.uhn.fhir.rest.server.interceptor.ResponseTerminologyTranslationSvc; import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices; import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; +import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; import ca.uhn.hapi.converters.canonical.VersionCanonicalizer; import org.hl7.fhir.common.hapi.validation.support.UnknownCodeSystemWarningValidationSupport; import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; @@ -891,4 +894,14 @@ public class JpaConfig { public IMdmClearHelperSvc helperSvc(IDeleteExpungeSvc theDeleteExpungeSvc) { return new MdmClearHelperSvcImpl(theDeleteExpungeSvc); } + + @Bean + public IResourceModifiedMessagePersistenceSvc subscriptionMessagePersistence( + FhirContext theFhirContext, + IResourceModifiedDao theIResourceModifiedDao, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService) { + return new ResourceModifiedMessagePersistenceSvcImpl( + theFhirContext, theIResourceModifiedDao, theDaoRegistry, theHapiTransactionService); + } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceModifiedDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceModifiedDao.java new file mode 100644 index 00000000000..011e4c60314 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceModifiedDao.java @@ -0,0 +1,42 @@ +package ca.uhn.fhir.jpa.dao.data; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.model.entity.IPersistedResourceModifiedMessage; +import ca.uhn.fhir.jpa.model.entity.PersistedResourceModifiedMessageEntityPK; +import ca.uhn.fhir.jpa.model.entity.ResourceModifiedEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface IResourceModifiedDao + extends JpaRepository, + IHapiFhirJpaRepository { + @Query("SELECT r FROM ResourceModifiedEntity r ORDER BY r.myCreatedTime ASC") + List findAllOrderedByCreatedTime(); + + @Modifying + @Query("delete from ResourceModifiedEntity r where r.myResourceModifiedEntityPK =:pk") + int removeById(@Param("pk") PersistedResourceModifiedMessageEntityPK thePK); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index 9d2734a3217..b68af9bc768 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -437,6 +437,16 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { .references(enversRevisionTable, revColumnName); } + { + Builder.BuilderAddTableByColumns resourceModifiedTable = + version.addTableByColumns("20230315.1", "HFJ_RESOURCE_MODIFIED", "RES_ID", "RES_VER"); + resourceModifiedTable.addColumn("RES_ID").nonNullable().type(ColumnTypeEnum.STRING, 256); + resourceModifiedTable.addColumn("RES_VER").nonNullable().type(ColumnTypeEnum.STRING, 8); + resourceModifiedTable.addColumn("CREATED_TIME").nonNullable().type(ColumnTypeEnum.DATE_TIMESTAMP); + resourceModifiedTable.addColumn("SUMMARY_MESSAGE").nonNullable().type(ColumnTypeEnum.STRING, 4000); + resourceModifiedTable.addColumn("RESOURCE_TYPE").nonNullable().type(ColumnTypeEnum.STRING, 40); + } + { // The pre-release already contains the long version of this column // We do this becausea doing a modifyColumn on Postgres (and possibly other RDBMS's) will fail with a nasty diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessagePersistenceSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessagePersistenceSvcImpl.java new file mode 100644 index 00000000000..86e85a85c9b --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/subscription/ResourceModifiedMessagePersistenceSvcImpl.java @@ -0,0 +1,181 @@ +package ca.uhn.fhir.jpa.subscription; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.i18n.Msg; +import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.dao.data.IResourceModifiedDao; +import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; +import ca.uhn.fhir.jpa.model.entity.IPersistedResourceModifiedMessage; +import ca.uhn.fhir.jpa.model.entity.IPersistedResourceModifiedMessagePK; +import ca.uhn.fhir.jpa.model.entity.PersistedResourceModifiedMessageEntityPK; +import ca.uhn.fhir.jpa.model.entity.ResourceModifiedEntity; +import ca.uhn.fhir.jpa.subscription.async.AsyncResourceModifiedSubmitterSvc; +import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; +import java.util.List; + +import static ca.uhn.fhir.jpa.model.entity.PersistedResourceModifiedMessageEntityPK.with; + +/** + * This implementer provides the capability to persist subscription messages for asynchronous submission + * to the subscription processing pipeline with the purpose of offering a retry mechanism + * upon submission failure (see @link {@link AsyncResourceModifiedSubmitterSvc}). + */ +public class ResourceModifiedMessagePersistenceSvcImpl implements IResourceModifiedMessagePersistenceSvc { + + private final FhirContext myFhirContext; + + private final IResourceModifiedDao myResourceModifiedDao; + + private final DaoRegistry myDaoRegistry; + + private final ObjectMapper myObjectMapper; + + private final HapiTransactionService myHapiTransactionService; + + private static final Logger ourLog = LoggerFactory.getLogger(ResourceModifiedMessagePersistenceSvcImpl.class); + + public ResourceModifiedMessagePersistenceSvcImpl( + FhirContext theFhirContext, + IResourceModifiedDao theResourceModifiedDao, + DaoRegistry theDaoRegistry, + HapiTransactionService theHapiTransactionService) { + myFhirContext = theFhirContext; + myResourceModifiedDao = theResourceModifiedDao; + myDaoRegistry = theDaoRegistry; + myHapiTransactionService = theHapiTransactionService; + myObjectMapper = new ObjectMapper(); + } + + @Override + public List findAllOrderedByCreatedTime() { + return myHapiTransactionService.withSystemRequest().execute(myResourceModifiedDao::findAllOrderedByCreatedTime); + } + + @Override + public IPersistedResourceModifiedMessage persist(ResourceModifiedMessage theMsg) { + ResourceModifiedEntity resourceModifiedEntity = createEntityFrom(theMsg); + return myResourceModifiedDao.save(resourceModifiedEntity); + } + + @Override + public ResourceModifiedMessage inflatePersistedResourceModifiedMessage( + IPersistedResourceModifiedMessage thePersistedResourceModifiedMessage) { + + return inflateResourceModifiedMessageFromEntity((ResourceModifiedEntity) thePersistedResourceModifiedMessage); + } + + @Override + public long getMessagePersistedCount() { + return myResourceModifiedDao.count(); + } + + @Override + public boolean deleteByPK(IPersistedResourceModifiedMessagePK theResourceModifiedPK) { + int removedCount = + myResourceModifiedDao.removeById((PersistedResourceModifiedMessageEntityPK) theResourceModifiedPK); + + return removedCount == 1; + } + + protected ResourceModifiedMessage inflateResourceModifiedMessageFromEntity( + ResourceModifiedEntity theResourceModifiedEntity) { + String resourcePid = + theResourceModifiedEntity.getResourceModifiedEntityPK().getResourcePid(); + String resourceVersion = + theResourceModifiedEntity.getResourceModifiedEntityPK().getResourceVersion(); + String resourceType = theResourceModifiedEntity.getResourceType(); + ResourceModifiedMessage retVal = + getPayloadLessMessageFromString(theResourceModifiedEntity.getSummaryResourceModifiedMessage()); + SystemRequestDetails systemRequestDetails = + new SystemRequestDetails().setRequestPartitionId(retVal.getPartitionId()); + + IdDt resourceIdDt = new IdDt(resourceType, resourcePid, resourceVersion); + IFhirResourceDao dao = myDaoRegistry.getResourceDao(resourceType); + + IBaseResource iBaseResource = dao.read(resourceIdDt, systemRequestDetails, true); + + retVal.setNewPayload(myFhirContext, iBaseResource); + + return retVal; + } + + ResourceModifiedEntity createEntityFrom(ResourceModifiedMessage theMsg) { + IIdType theMsgId = theMsg.getPayloadId(myFhirContext); + + ResourceModifiedEntity resourceModifiedEntity = new ResourceModifiedEntity(); + resourceModifiedEntity.setResourceModifiedEntityPK(with(theMsgId.getIdPart(), theMsgId.getVersionIdPart())); + + String partialModifiedMessage = getPayloadLessMessageAsString(theMsg); + resourceModifiedEntity.setSummaryResourceModifiedMessage(partialModifiedMessage); + resourceModifiedEntity.setResourceType(theMsgId.getResourceType()); + resourceModifiedEntity.setCreatedTime(new Date()); + + return resourceModifiedEntity; + } + + private ResourceModifiedMessage getPayloadLessMessageFromString(String thePayloadLessMessage) { + try { + return myObjectMapper.readValue(thePayloadLessMessage, ResourceModifiedMessage.class); + } catch (JsonProcessingException e) { + throw new ConfigurationException(Msg.code(2334) + "Failed to json deserialize payloadless message", e); + } + } + + private String getPayloadLessMessageAsString(ResourceModifiedMessage theMsg) { + ResourceModifiedMessage tempMessage = new PayloadLessResourceModifiedMessage(theMsg); + + try { + return myObjectMapper.writeValueAsString(tempMessage); + } catch (JsonProcessingException e) { + throw new ConfigurationException(Msg.code(2335) + "Failed to serialize empty ResourceModifiedMessage", e); + } + } + + private static class PayloadLessResourceModifiedMessage extends ResourceModifiedMessage { + + public PayloadLessResourceModifiedMessage(ResourceModifiedMessage theMsg) { + this.myPayloadId = theMsg.getPayloadId(); + this.myPayloadVersion = theMsg.getPayloadVersion(); + setSubscriptionId(theMsg.getSubscriptionId()); + setMediaType(theMsg.getMediaType()); + setOperationType(theMsg.getOperationType()); + setPartitionId(theMsg.getPartitionId()); + setTransactionId(theMsg.getTransactionId()); + setMessageKey(theMsg.getMessageKeyOrNull()); + copyAdditionalPropertiesFrom(theMsg); + } + } +} diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml index 8c9d5c580d7..8d80a304cb6 100644 --- a/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-elastic-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-hfql/pom.xml b/hapi-fhir-jpaserver-hfql/pom.xml index 4bb5263f797..3a208401d5e 100644 --- a/hapi-fhir-jpaserver-hfql/pom.xml +++ b/hapi-fhir-jpaserver-hfql/pom.xml @@ -3,7 +3,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-ips/pom.xml b/hapi-fhir-jpaserver-ips/pom.xml index 9875b89f791..d6a3923ba24 100644 --- a/hapi-fhir-jpaserver-ips/pom.xml +++ b/hapi-fhir-jpaserver-ips/pom.xml @@ -3,7 +3,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java index c859dc935f7..db7239b2848 100644 --- a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java +++ b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java @@ -9,6 +9,7 @@ import ca.uhn.fhir.jpa.ips.strategy.DefaultIpsGenerationStrategy; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.valueset.BundleEntrySearchModeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.SimpleBundleProvider; @@ -499,7 +500,7 @@ public class IpsGeneratorSvcImplTest { IFhirResourceDao patientDao = registerResourceDaoWithNoData(Patient.class); Patient patient = new Patient(); patient.setId(PATIENT_ID); - when(patientDao.read(any(), any())).thenReturn(patient); + when(patientDao.read(any(), any(RequestDetails.class))).thenReturn(patient); } private void registerRemainingResourceDaos() { diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-mdm/pom.xml index 5d815141aa8..73f931a370e 100644 --- a/hapi-fhir-jpaserver-mdm/pom.xml +++ b/hapi-fhir-jpaserver-mdm/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/MdmSubscriptionLoaderTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/MdmSubscriptionLoaderTest.java index 9f18fbfa03d..91a79df2276 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/MdmSubscriptionLoaderTest.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/MdmSubscriptionLoaderTest.java @@ -74,7 +74,7 @@ class MdmSubscriptionLoaderTest { Subscription subscription = new Subscription(); IdType id = new IdType("2401"); subscription.setIdElement(id); - when(mySubscriptionDao.read(eq(id), any())).thenThrow(new ResourceGoneException("")); + when(mySubscriptionDao.read(eq(id), any(RequestDetails.class))).thenThrow(new ResourceGoneException("")); mySvc.updateIfNotPresent(subscription); verify(mySubscriptionDao).update(eq(subscription), any(RequestDetails.class)); } @@ -84,7 +84,7 @@ class MdmSubscriptionLoaderTest { Subscription subscription = new Subscription(); IdType id = new IdType("2401"); subscription.setIdElement(id); - when(mySubscriptionDao.read(eq(id), any())).thenThrow(new ResourceNotFoundException("")); + when(mySubscriptionDao.read(eq(id), any(RequestDetails.class))).thenThrow(new ResourceNotFoundException("")); mySvc.updateIfNotPresent(subscription); verify(mySubscriptionDao).update(eq(subscription), any(RequestDetails.class)); } @@ -94,7 +94,7 @@ class MdmSubscriptionLoaderTest { Subscription subscription = new Subscription(); IdType id = new IdType("2401"); subscription.setIdElement(id); - when(mySubscriptionDao.read(eq(id), any())).thenReturn(subscription); + when(mySubscriptionDao.read(eq(id), any(RequestDetails.class))).thenReturn(subscription); mySvc.updateIfNotPresent(subscription); verify(mySubscriptionDao, never()).update(any(), any(RequestDetails.class)); } @@ -106,7 +106,7 @@ class MdmSubscriptionLoaderTest { when(myMdmSettings.getMdmRules()).thenReturn(mdmRulesJson); when(myChannelNamer.getChannelName(any(), any())).thenReturn("Test"); when(myDaoRegistry.getResourceDao(eq("Subscription"))).thenReturn(mySubscriptionDao); - when(mySubscriptionDao.read(any(), any())).thenThrow(new ResourceGoneException("")); + when(mySubscriptionDao.read(any(), any(RequestDetails.class))).thenThrow(new ResourceGoneException("")); mySvc.daoUpdateMdmSubscriptions(); diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/TestMdmConfigR4.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/TestMdmConfigR4.java index 36aa1990823..2b018735cb4 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/TestMdmConfigR4.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/config/TestMdmConfigR4.java @@ -4,12 +4,13 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.mdm.helper.MdmHelperR4; import ca.uhn.fhir.jpa.subscription.channel.config.SubscriptionChannelConfig; import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig; +import ca.uhn.fhir.jpa.test.config.TestSubscriptionMatcherInterceptorConfig; import org.hl7.fhir.dstu2.model.Subscription; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; -@Import({SubscriptionSubmitterConfig.class, SubscriptionChannelConfig.class}) +@Import({TestSubscriptionMatcherInterceptorConfig.class, SubscriptionSubmitterConfig.class, SubscriptionChannelConfig.class}) public class TestMdmConfigR4 extends BaseTestMdmConfig { @Bean MdmHelperR4 mdmHelperR4() { diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index d807924bf6b..8cda6effc22 100644 --- a/hapi-fhir-jpaserver-model/pom.xml +++ b/hapi-fhir-jpaserver-model/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IPersistedResourceModifiedMessage.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IPersistedResourceModifiedMessage.java new file mode 100644 index 00000000000..bc91e918aec --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IPersistedResourceModifiedMessage.java @@ -0,0 +1,27 @@ +/*- + * #%L + * HAPI FHIR JPA Model + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.model.entity; + +public interface IPersistedResourceModifiedMessage { + + IPersistedResourceModifiedMessagePK getPersistedResourceModifiedMessagePk(); + + String getResourceType(); +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IPersistedResourceModifiedMessagePK.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IPersistedResourceModifiedMessagePK.java new file mode 100644 index 00000000000..00fc70aa2cc --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/IPersistedResourceModifiedMessagePK.java @@ -0,0 +1,27 @@ +/*- + * #%L + * HAPI FHIR JPA Model + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.model.entity; + +public interface IPersistedResourceModifiedMessagePK { + + String getResourcePid(); + + String getResourceVersion(); +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PersistedResourceModifiedMessageEntityPK.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PersistedResourceModifiedMessageEntityPK.java new file mode 100644 index 00000000000..4cf7fa42516 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/PersistedResourceModifiedMessageEntityPK.java @@ -0,0 +1,78 @@ +package ca.uhn.fhir.jpa.model.entity; + +/*- + * #%L + * HAPI FHIR JPA Model + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.io.Serializable; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Embeddable; + +@Embeddable +public class PersistedResourceModifiedMessageEntityPK implements IPersistedResourceModifiedMessagePK, Serializable { + + @Column(name = "RES_ID", length = 256, nullable = false) + private String myResourcePid; + + @Column(name = "RES_VER", length = 8, nullable = false) + private String myResourceVersion; + + public String getResourcePid() { + return myResourcePid; + } + + public PersistedResourceModifiedMessageEntityPK setResourcePid(String theResourcePid) { + myResourcePid = theResourcePid; + return this; + } + + public String getResourceVersion() { + return myResourceVersion; + } + + public PersistedResourceModifiedMessageEntityPK setResourceVersion(String theResourceVersion) { + myResourceVersion = theResourceVersion; + return this; + } + + public static PersistedResourceModifiedMessageEntityPK with(String theResourcePid, String theResourceVersion) { + return new PersistedResourceModifiedMessageEntityPK() + .setResourcePid(theResourcePid) + .setResourceVersion(theResourceVersion); + } + + @Override + public boolean equals(Object theO) { + if (this == theO) return true; + if (theO == null || getClass() != theO.getClass()) return false; + PersistedResourceModifiedMessageEntityPK that = (PersistedResourceModifiedMessageEntityPK) theO; + return myResourcePid.equals(that.myResourcePid) && myResourceVersion.equals(that.myResourceVersion); + } + + @Override + public int hashCode() { + return Objects.hash(myResourcePid, myResourceVersion); + } + + @Override + public String toString() { + return myResourcePid + "/" + myResourceVersion; + } +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceModifiedEntity.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceModifiedEntity.java new file mode 100644 index 00000000000..11ec35ff436 --- /dev/null +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceModifiedEntity.java @@ -0,0 +1,99 @@ +package ca.uhn.fhir.jpa.model.entity; + +/*- + * #%L + * HAPI FHIR JPA Model + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.io.Serializable; +import java.util.Date; +import javax.persistence.Column; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +/** + * This class describes how a resourceModifiedMessage is stored for later processing in the event where + * submission to the subscription processing pipeline would fail. The persisted message does not include a + * payload (resource) as an in-memory version of the same message would. Instead, it points to a payload + * through the entity primary key {@link PersistedResourceModifiedMessageEntityPK} which is composed + * of the resource Pid and current version. + */ +@Entity +@Table(name = "HFJ_RESOURCE_MODIFIED") +public class ResourceModifiedEntity implements IPersistedResourceModifiedMessage, Serializable { + + public static final int MESSAGE_LENGTH = 4000; + + @EmbeddedId + private PersistedResourceModifiedMessageEntityPK myResourceModifiedEntityPK; + + @Column(name = "SUMMARY_MESSAGE", length = MESSAGE_LENGTH, nullable = false) + private String mySummaryResourceModifiedMessage; + + @Column(name = "CREATED_TIME", nullable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date myCreatedTime; + + @Column(name = "RESOURCE_TYPE", length = ResourceTable.RESTYPE_LEN, nullable = false) + private String myResourceType; + + public PersistedResourceModifiedMessageEntityPK getResourceModifiedEntityPK() { + return myResourceModifiedEntityPK; + } + + public ResourceModifiedEntity setResourceModifiedEntityPK( + PersistedResourceModifiedMessageEntityPK theResourceModifiedEntityPK) { + myResourceModifiedEntityPK = theResourceModifiedEntityPK; + return this; + } + + @Override + public String getResourceType() { + return myResourceType; + } + + public ResourceModifiedEntity setResourceType(String theResourceType) { + myResourceType = theResourceType; + return this; + } + + public Date getCreatedTime() { + return myCreatedTime; + } + + public void setCreatedTime(Date theCreatedTime) { + myCreatedTime = theCreatedTime; + } + + public String getSummaryResourceModifiedMessage() { + return mySummaryResourceModifiedMessage; + } + + public ResourceModifiedEntity setSummaryResourceModifiedMessage(String theSummaryResourceModifiedMessage) { + mySummaryResourceModifiedMessage = theSummaryResourceModifiedMessage; + return this; + } + + @Override + public IPersistedResourceModifiedMessagePK getPersistedResourceModifiedMessagePk() { + return myResourceModifiedEntityPK; + } +} diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/StorageSettings.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/StorageSettings.java index 0bdf6704fae..42a2b393f53 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/StorageSettings.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/StorageSettings.java @@ -26,6 +26,7 @@ import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.rest.server.interceptor.ResponseTerminologyTranslationSvc; import ca.uhn.fhir.util.HapiExtensions; import com.google.common.annotations.VisibleForTesting; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.Validate; import org.hl7.fhir.dstu2.model.Subscription; import org.hl7.fhir.instance.model.api.IPrimitiveType; @@ -781,6 +782,15 @@ public class StorageSettings { return Collections.unmodifiableSet(mySupportedSubscriptionTypes); } + /** + * Indicate whether a subscription channel type is supported by this server. + * + * @return true if at least one subscription channel type is supported by this server false otherwise. + */ + public boolean hasSupportedSubscriptionTypes() { + return CollectionUtils.isNotEmpty(mySupportedSubscriptionTypes); + } + @VisibleForTesting public void clearSupportedSubscriptionTypesForUnitTest() { mySupportedSubscriptionTypes.clear(); diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index dbd906ca326..5d37b0c1c89 100755 --- a/hapi-fhir-jpaserver-searchparam/pom.xml +++ b/hapi-fhir-jpaserver-searchparam/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index 3415333713b..89d6bcd2617 100644 --- a/hapi-fhir-jpaserver-subscription/pom.xml +++ b/hapi-fhir-jpaserver-subscription/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/async/AsyncResourceModifiedProcessingSchedulerSvc.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/async/AsyncResourceModifiedProcessingSchedulerSvc.java new file mode 100644 index 00000000000..50f609e65a4 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/async/AsyncResourceModifiedProcessingSchedulerSvc.java @@ -0,0 +1,66 @@ +package ca.uhn.fhir.jpa.subscription.async; + +/*- + * #%L + * HAPI FHIR Subscription Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.model.sched.HapiJob; +import ca.uhn.fhir.jpa.model.sched.IHasScheduledJobs; +import ca.uhn.fhir.jpa.model.sched.ISchedulerService; +import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition; +import org.quartz.JobExecutionContext; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * This service is responsible for scheduling a job that will submit messages + * to the subscription processing pipeline at a given interval. + */ +public class AsyncResourceModifiedProcessingSchedulerSvc implements IHasScheduledJobs { + + public static final long DEFAULT_SUBMISSION_INTERVAL_IN_MS = 5000; + + public long mySubmissionIntervalInMilliSeconds; + + public AsyncResourceModifiedProcessingSchedulerSvc() { + this(DEFAULT_SUBMISSION_INTERVAL_IN_MS); + } + + public AsyncResourceModifiedProcessingSchedulerSvc(long theSubmissionIntervalInMilliSeconds) { + mySubmissionIntervalInMilliSeconds = theSubmissionIntervalInMilliSeconds; + } + + @Override + public void scheduleJobs(ISchedulerService theSchedulerService) { + ScheduledJobDefinition jobDetail = new ScheduledJobDefinition(); + jobDetail.setId(getClass().getName()); + jobDetail.setJobClass(AsyncResourceModifiedProcessingSchedulerSvc.Job.class); + + theSchedulerService.scheduleClusteredJob(mySubmissionIntervalInMilliSeconds, jobDetail); + } + + public static class Job implements HapiJob { + @Autowired + private AsyncResourceModifiedSubmitterSvc myAsyncResourceModifiedSubmitterSvc; + + @Override + public void execute(JobExecutionContext theContext) { + myAsyncResourceModifiedSubmitterSvc.runDeliveryPass(); + } + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/async/AsyncResourceModifiedSubmitterSvc.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/async/AsyncResourceModifiedSubmitterSvc.java new file mode 100644 index 00000000000..20befe08af0 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/async/AsyncResourceModifiedSubmitterSvc.java @@ -0,0 +1,67 @@ +package ca.uhn.fhir.jpa.subscription.async; + +/*- + * #%L + * HAPI FHIR Subscription Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.model.entity.IPersistedResourceModifiedMessage; +import ca.uhn.fhir.subscription.api.IResourceModifiedConsumerWithRetries; +import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * The purpose of this service is to submit messages to the processing pipeline for which previous attempts at + * submission has failed. See also {@link AsyncResourceModifiedProcessingSchedulerSvc} and {@link IResourceModifiedMessagePersistenceSvc}. + * + */ +public class AsyncResourceModifiedSubmitterSvc { + private static final Logger ourLog = LoggerFactory.getLogger(AsyncResourceModifiedSubmitterSvc.class); + + private final IResourceModifiedMessagePersistenceSvc myResourceModifiedMessagePersistenceSvc; + private final IResourceModifiedConsumerWithRetries myResourceModifiedConsumer; + + public AsyncResourceModifiedSubmitterSvc( + IResourceModifiedMessagePersistenceSvc theResourceModifiedMessagePersistenceSvc, + IResourceModifiedConsumerWithRetries theResourceModifiedConsumer) { + myResourceModifiedMessagePersistenceSvc = theResourceModifiedMessagePersistenceSvc; + myResourceModifiedConsumer = theResourceModifiedConsumer; + } + + public void runDeliveryPass() { + + List allPersistedResourceModifiedMessages = + myResourceModifiedMessagePersistenceSvc.findAllOrderedByCreatedTime(); + ourLog.debug( + "Attempting to submit {} resources to consumer channel.", allPersistedResourceModifiedMessages.size()); + + for (IPersistedResourceModifiedMessage persistedResourceModifiedMessage : + allPersistedResourceModifiedMessages) { + + boolean wasProcessed = + myResourceModifiedConsumer.submitPersisedResourceModifiedMessage(persistedResourceModifiedMessage); + + if (!wasProcessed) { + break; + } + } + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionMatcherInterceptorConfig.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionMatcherInterceptorConfig.java new file mode 100644 index 00000000000..c0cbb269603 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionMatcherInterceptorConfig.java @@ -0,0 +1,50 @@ +/*- + * #%L + * HAPI FHIR Subscription Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.subscription.submit.config; + +import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.SynchronousSubscriptionMatcherInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +import static ca.uhn.fhir.jpa.sched.BaseSchedulerServiceImpl.SCHEDULING_DISABLED; + +@Configuration +public class SubscriptionMatcherInterceptorConfig { + + @Autowired + private Environment myEnvironment; + + @Bean + public SubscriptionMatcherInterceptor subscriptionMatcherInterceptor() { + if (isSchedulingDisabledForTests()) { + return new SynchronousSubscriptionMatcherInterceptor(); + } + + return new SubscriptionMatcherInterceptor(); + } + + private boolean isSchedulingDisabledForTests() { + String schedulingDisabled = myEnvironment.getProperty(SCHEDULING_DISABLED); + return "true".equals(schedulingDisabled); + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionSubmitterConfig.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionSubmitterConfig.java index 0a842a8ee3f..22346f68c0f 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionSubmitterConfig.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/config/SubscriptionSubmitterConfig.java @@ -20,14 +20,21 @@ package ca.uhn.fhir.jpa.subscription.submit.config; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; +import ca.uhn.fhir.jpa.model.entity.StorageSettings; +import ca.uhn.fhir.jpa.subscription.async.AsyncResourceModifiedProcessingSchedulerSvc; +import ca.uhn.fhir.jpa.subscription.async.AsyncResourceModifiedSubmitterSvc; +import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory; import ca.uhn.fhir.jpa.subscription.match.matcher.matching.SubscriptionStrategyEvaluator; import ca.uhn.fhir.jpa.subscription.model.config.SubscriptionModelConfig; -import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionQueryValidator; import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionSubmitInterceptorLoader; import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionValidatingInterceptor; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.subscription.triggering.ISubscriptionTriggeringSvc; import ca.uhn.fhir.jpa.subscription.triggering.SubscriptionTriggeringSvcImpl; +import ca.uhn.fhir.subscription.api.IResourceModifiedConsumerWithRetries; +import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -38,14 +45,9 @@ import org.springframework.context.annotation.Lazy; * matching queue for processing */ @Configuration -@Import(SubscriptionModelConfig.class) +@Import({SubscriptionModelConfig.class, SubscriptionMatcherInterceptorConfig.class}) public class SubscriptionSubmitterConfig { - @Bean - public SubscriptionMatcherInterceptor subscriptionMatcherInterceptor() { - return new SubscriptionMatcherInterceptor(); - } - @Bean public SubscriptionValidatingInterceptor subscriptionValidatingInterceptor() { return new SubscriptionValidatingInterceptor(); @@ -67,4 +69,31 @@ public class SubscriptionSubmitterConfig { public ISubscriptionTriggeringSvc subscriptionTriggeringSvc() { return new SubscriptionTriggeringSvcImpl(); } + + @Bean + public ResourceModifiedSubmitterSvc resourceModifiedSvc( + IHapiTransactionService theHapiTransactionService, + IResourceModifiedMessagePersistenceSvc theResourceModifiedMessagePersistenceSvc, + SubscriptionChannelFactory theSubscriptionChannelFactory, + StorageSettings theStorageSettings) { + + return new ResourceModifiedSubmitterSvc( + theStorageSettings, + theSubscriptionChannelFactory, + theResourceModifiedMessagePersistenceSvc, + theHapiTransactionService); + } + + @Bean + public AsyncResourceModifiedProcessingSchedulerSvc asyncResourceModifiedProcessingSchedulerSvc() { + return new AsyncResourceModifiedProcessingSchedulerSvc(); + } + + @Bean + public AsyncResourceModifiedSubmitterSvc asyncResourceModifiedSubmitterSvc( + IResourceModifiedMessagePersistenceSvc theIResourceModifiedMessagePersistenceSvc, + IResourceModifiedConsumerWithRetries theResourceModifiedConsumer) { + return new AsyncResourceModifiedSubmitterSvc( + theIResourceModifiedMessagePersistenceSvc, theResourceModifiedConsumer); + } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptor.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptor.java index d051cca1240..eb444079a47 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptor.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptor.java @@ -28,31 +28,26 @@ import ca.uhn.fhir.interceptor.api.Pointcut; import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; -import ca.uhn.fhir.jpa.subscription.channel.api.ChannelProducerSettings; -import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannel; -import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory; -import ca.uhn.fhir.jpa.subscription.match.matcher.matching.IResourceModifiedConsumer; -import ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionMatchingSubscriber; -import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.messaging.BaseResourceMessage; import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster; -import com.google.common.annotations.VisibleForTesting; -import org.apache.commons.lang3.Validate; +import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; import org.hl7.fhir.instance.model.api.IBaseResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.context.event.EventListener; -import org.springframework.messaging.MessageChannel; -import org.springframework.transaction.support.TransactionSynchronizationAdapter; -import org.springframework.transaction.support.TransactionSynchronizationManager; -import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static java.util.Objects.isNull; +import static org.apache.commons.lang3.StringUtils.isBlank; +/** + * + * This interceptor is responsible for submitting operations on resources to the subscription pipeline. + * + */ @Interceptor -public class SubscriptionMatcherInterceptor implements IResourceModifiedConsumer { +public class SubscriptionMatcherInterceptor { private static final Logger ourLog = LoggerFactory.getLogger(SubscriptionMatcherInterceptor.class); @Autowired @@ -61,16 +56,14 @@ public class SubscriptionMatcherInterceptor implements IResourceModifiedConsumer @Autowired private IInterceptorBroadcaster myInterceptorBroadcaster; - @Autowired - private SubscriptionChannelFactory mySubscriptionChannelFactory; - @Autowired private StorageSettings myStorageSettings; @Autowired private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; - private volatile MessageChannel myMatchingChannel; + @Autowired + private IResourceModifiedMessagePersistenceSvc myResourceModifiedMessagePersistenceSvc; /** * Constructor @@ -79,122 +72,91 @@ public class SubscriptionMatcherInterceptor implements IResourceModifiedConsumer super(); } - @EventListener(classes = {ContextRefreshedEvent.class}) - public void startIfNeeded() { - if (myStorageSettings.getSupportedSubscriptionTypes().isEmpty()) { - ourLog.debug( - "Subscriptions are disabled on this server. Skipping {} channel creation.", - SubscriptionMatchingSubscriber.SUBSCRIPTION_MATCHING_CHANNEL_NAME); - return; - } - if (myMatchingChannel == null) { - myMatchingChannel = mySubscriptionChannelFactory.newMatchingSendingChannel( - SubscriptionMatchingSubscriber.SUBSCRIPTION_MATCHING_CHANNEL_NAME, getChannelProducerSettings()); - } - } - @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED) public void resourceCreated(IBaseResource theResource, RequestDetails theRequest) { - startIfNeeded(); - submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE, theRequest); + + processResourceModifiedEvent(theResource, ResourceModifiedMessage.OperationTypeEnum.CREATE, theRequest); } @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_DELETED) public void resourceDeleted(IBaseResource theResource, RequestDetails theRequest) { - startIfNeeded(); - submitResourceModified(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE, theRequest); + + processResourceModifiedEvent(theResource, ResourceModifiedMessage.OperationTypeEnum.DELETE, theRequest); } @Hook(Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED) public void resourceUpdated(IBaseResource theOldResource, IBaseResource theNewResource, RequestDetails theRequest) { - startIfNeeded(); - if (!myStorageSettings.isTriggerSubscriptionsForNonVersioningChanges()) { - if (theOldResource != null && theNewResource != null) { - String oldVersion = theOldResource.getIdElement().getVersionIdPart(); - String newVersion = theNewResource.getIdElement().getVersionIdPart(); - if (isNotBlank(oldVersion) && isNotBlank(newVersion) && oldVersion.equals(newVersion)) { - return; - } - } + boolean dontTriggerSubscriptionWhenVersionsAreTheSame = + !myStorageSettings.isTriggerSubscriptionsForNonVersioningChanges(); + boolean resourceVersionsAreTheSame = isSameResourceVersion(theOldResource, theNewResource); + + if (dontTriggerSubscriptionWhenVersionsAreTheSame && resourceVersionsAreTheSame) { + return; } - submitResourceModified(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE, theRequest); + processResourceModifiedEvent(theNewResource, ResourceModifiedMessage.OperationTypeEnum.UPDATE, theRequest); } /** * This is an internal API - Use with caution! + * + * This method will create a {@link ResourceModifiedMessage}, persist it and arrange for its delivery to the + * subscription pipeline after the resource was committed. The message is persisted to provide asynchronous submission + * in the event where submission would fail. */ - @Override - public void submitResourceModified( + protected void processResourceModifiedEvent( IBaseResource theNewResource, ResourceModifiedMessage.OperationTypeEnum theOperationType, RequestDetails theRequest) { - // Even though the resource is being written, the subscription will be interacting with it by effectively - // "reading" it so we set the RequestPartitionId as a read request - RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForRead( - theRequest, theNewResource.getIdElement().getResourceType(), theNewResource.getIdElement()); - ResourceModifiedMessage msg = new ResourceModifiedMessage( - myFhirContext, theNewResource, theOperationType, theRequest, requestPartitionId); + + ResourceModifiedMessage msg = createResourceModifiedMessage(theNewResource, theOperationType, theRequest); // Interceptor call: SUBSCRIPTION_RESOURCE_MODIFIED HookParams params = new HookParams().add(ResourceModifiedMessage.class, msg); boolean outcome = CompositeInterceptorBroadcaster.doCallHooks( myInterceptorBroadcaster, theRequest, Pointcut.SUBSCRIPTION_RESOURCE_MODIFIED, params); + if (!outcome) { return; } - submitResourceModified(msg); + processResourceModifiedMessage(msg); } - /** - * This is an internal API - Use with caution! - */ - @Override - public void submitResourceModified(final ResourceModifiedMessage theMsg) { - /* - * We only want to submit the message to the processing queue once the - * transaction is committed. We do this in order to make sure that the - * data is actually in the DB, in case it's the database matcher. - */ - if (TransactionSynchronizationManager.isSynchronizationActive()) { - TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { - @Override - public int getOrder() { - return 0; - } + protected void processResourceModifiedMessage(ResourceModifiedMessage theResourceModifiedMessage) { + // persist the message for async submission to the processing pipeline. see {@link + // AsyncResourceModifiedProcessingSchedulerSvc} + myResourceModifiedMessagePersistenceSvc.persist(theResourceModifiedMessage); + } - @Override - public void afterCommit() { - sendToProcessingChannel(theMsg); - } - }); - } else { - sendToProcessingChannel(theMsg); + protected ResourceModifiedMessage createResourceModifiedMessage( + IBaseResource theNewResource, + BaseResourceMessage.OperationTypeEnum theOperationType, + RequestDetails theRequest) { + // Even though the resource is being written, the subscription will be interacting with it by effectively + // "reading" it so we set the RequestPartitionId as a read request + RequestPartitionId requestPartitionId = myRequestPartitionHelperSvc.determineReadPartitionForRequestForRead( + theRequest, theNewResource.getIdElement().getResourceType(), theNewResource.getIdElement()); + return new ResourceModifiedMessage( + myFhirContext, theNewResource, theOperationType, theRequest, requestPartitionId); + } + + private boolean isSameResourceVersion(IBaseResource theOldResource, IBaseResource theNewResource) { + if (isNull(theOldResource) || isNull(theNewResource)) { + return false; } - } - protected void sendToProcessingChannel(final ResourceModifiedMessage theMessage) { - ourLog.trace("Sending resource modified message to processing channel"); - Validate.notNull( - myMatchingChannel, - "A SubscriptionMatcherInterceptor has been registered without calling start() on it."); - myMatchingChannel.send(new ResourceModifiedJsonMessage(theMessage)); - } + String oldVersion = theOldResource.getIdElement().getVersionIdPart(); + String newVersion = theNewResource.getIdElement().getVersionIdPart(); - private ChannelProducerSettings getChannelProducerSettings() { - ChannelProducerSettings channelProducerSettings = new ChannelProducerSettings(); - channelProducerSettings.setQualifyChannelName(myStorageSettings.isQualifySubscriptionMatchingChannelName()); - return channelProducerSettings; + if (isBlank(oldVersion) || isBlank(newVersion)) { + return false; + } + + return oldVersion.equals(newVersion); } public void setFhirContext(FhirContext theCtx) { myFhirContext = theCtx; } - - @VisibleForTesting - public LinkedBlockingChannel getProcessingChannelForUnitTest() { - startIfNeeded(); - return (LinkedBlockingChannel) myMatchingChannel; - } } diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SynchronousSubscriptionMatcherInterceptor.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SynchronousSubscriptionMatcherInterceptor.java new file mode 100644 index 00000000000..33d655d6a78 --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SynchronousSubscriptionMatcherInterceptor.java @@ -0,0 +1,59 @@ +/*- + * #%L + * HAPI FHIR Subscription Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.subscription.submit.interceptor; + +import ca.uhn.fhir.jpa.subscription.async.AsyncResourceModifiedProcessingSchedulerSvc; +import ca.uhn.fhir.jpa.subscription.match.matcher.matching.IResourceModifiedConsumer; +import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.support.TransactionSynchronizationAdapter; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * The purpose of this interceptor is to synchronously submit ResourceModifiedMessage to the + * subscription processing pipeline, ie, as part of processing the operation on a resource. + * It is meant to replace the SubscriptionMatcherInterceptor in integrated tests where + * scheduling is disabled. See {@link AsyncResourceModifiedProcessingSchedulerSvc} + * for further details on asynchronous submissions. + */ +public class SynchronousSubscriptionMatcherInterceptor extends SubscriptionMatcherInterceptor { + + @Autowired + private IResourceModifiedConsumer myResourceModifiedConsumer; + + @Override + protected void processResourceModifiedMessage(ResourceModifiedMessage theResourceModifiedMessage) { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { + @Override + public int getOrder() { + return 0; + } + + @Override + public void afterCommit() { + myResourceModifiedConsumer.submitResourceModified(theResourceModifiedMessage); + } + }); + } else { + myResourceModifiedConsumer.submitResourceModified(theResourceModifiedMessage); + } + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/svc/ResourceModifiedSubmitterSvc.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/svc/ResourceModifiedSubmitterSvc.java new file mode 100644 index 00000000000..7d768beefce --- /dev/null +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/submit/svc/ResourceModifiedSubmitterSvc.java @@ -0,0 +1,230 @@ +package ca.uhn.fhir.jpa.subscription.submit.svc; + +/*- + * #%L + * HAPI FHIR Subscription Server + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; +import ca.uhn.fhir.jpa.model.entity.IPersistedResourceModifiedMessage; +import ca.uhn.fhir.jpa.model.entity.IPersistedResourceModifiedMessagePK; +import ca.uhn.fhir.jpa.model.entity.StorageSettings; +import ca.uhn.fhir.jpa.subscription.channel.api.ChannelProducerSettings; +import ca.uhn.fhir.jpa.subscription.channel.api.IChannelProducer; +import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory; +import ca.uhn.fhir.jpa.subscription.match.matcher.matching.IResourceModifiedConsumer; +import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage; +import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; +import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.subscription.api.IResourceModifiedConsumerWithRetries; +import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; +import org.apache.commons.lang3.Validate; +import org.hl7.fhir.r5.model.IdType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessageDeliveryException; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.support.TransactionCallback; + +import java.util.Optional; + +import static ca.uhn.fhir.jpa.subscription.match.matcher.subscriber.SubscriptionMatchingSubscriber.SUBSCRIPTION_MATCHING_CHANNEL_NAME; + +/** + * This service provides two distinct contexts in which it submits messages to the subscription pipeline. + * + * It implements {@link IResourceModifiedConsumer} for synchronous submissions where retry upon failures is not required. + * + * It implements {@link IResourceModifiedConsumerWithRetries} for synchronous submissions performed as part of processing + * an operation on a resource (see {@link SubscriptionMatcherInterceptor}). Submissions in such context require retries + * upon submission failure. + * + * + */ +public class ResourceModifiedSubmitterSvc implements IResourceModifiedConsumer, IResourceModifiedConsumerWithRetries { + + private static final Logger ourLog = LoggerFactory.getLogger(ResourceModifiedSubmitterSvc.class); + private volatile MessageChannel myMatchingChannel; + + private final StorageSettings myStorageSettings; + private final SubscriptionChannelFactory mySubscriptionChannelFactory; + private final IResourceModifiedMessagePersistenceSvc myResourceModifiedMessagePersistenceSvc; + private final IHapiTransactionService myHapiTransactionService; + + @EventListener(classes = {ContextRefreshedEvent.class}) + public void startIfNeeded() { + if (!myStorageSettings.hasSupportedSubscriptionTypes()) { + ourLog.debug( + "Subscriptions are disabled on this server. Skipping {} channel creation.", + SUBSCRIPTION_MATCHING_CHANNEL_NAME); + return; + } + if (myMatchingChannel == null) { + myMatchingChannel = mySubscriptionChannelFactory.newMatchingSendingChannel( + SUBSCRIPTION_MATCHING_CHANNEL_NAME, getChannelProducerSettings()); + } + } + + public ResourceModifiedSubmitterSvc( + StorageSettings theStorageSettings, + SubscriptionChannelFactory theSubscriptionChannelFactory, + IResourceModifiedMessagePersistenceSvc resourceModifiedMessagePersistenceSvc, + IHapiTransactionService theHapiTransactionService) { + myStorageSettings = theStorageSettings; + mySubscriptionChannelFactory = theSubscriptionChannelFactory; + myResourceModifiedMessagePersistenceSvc = resourceModifiedMessagePersistenceSvc; + myHapiTransactionService = theHapiTransactionService; + } + + /** + * @inheritDoc + * Submit a message to the broker without retries. + * + * Implementation of the {@link IResourceModifiedConsumer} + * + */ + @Override + public void submitResourceModified(ResourceModifiedMessage theMsg) { + startIfNeeded(); + + ourLog.trace("Sending resource modified message to processing channel"); + Validate.notNull( + myMatchingChannel, + "A SubscriptionMatcherInterceptor has been registered without calling start() on it."); + myMatchingChannel.send(new ResourceModifiedJsonMessage(theMsg)); + } + + /** + * This method will inflate the ResourceModifiedMessage represented by the IPersistedResourceModifiedMessage and attempts + * to submit it to the subscription processing pipeline. + * + * If submission succeeds, the IPersistedResourceModifiedMessage is deleted and true is returned. In the event where submission + * fails, we return false and the IPersistedResourceModifiedMessage is rollback for later re-submission. + * + * @param thePersistedResourceModifiedMessage A ResourceModifiedMessage in it's IPersistedResourceModifiedMessage that requires submission. + * @return Whether the message was successfully submitted to the broker. + */ + @Override + public boolean submitPersisedResourceModifiedMessage( + IPersistedResourceModifiedMessage thePersistedResourceModifiedMessage) { + return myHapiTransactionService + .withSystemRequest() + .withPropagation(Propagation.REQUIRES_NEW) + .execute(doProcessResourceModifiedInTransaction(thePersistedResourceModifiedMessage)); + } + + /** + * This method is the cornerstone in the submit and retry upon failure mechanism for messages needing submission to the subscription processing pipeline. + * It requires execution in a transaction for rollback of deleting the persistedResourceModifiedMessage pointed to by thePersistedResourceModifiedMessage + * in the event where submission would fail. + * + * @param thePersistedResourceModifiedMessage the primary key pointing to the persisted version (IPersistedResourceModifiedMessage) of a ResourceModifiedMessage needing submission + * @return true upon successful submission, false otherwise. + */ + protected TransactionCallback doProcessResourceModifiedInTransaction( + IPersistedResourceModifiedMessage thePersistedResourceModifiedMessage) { + return theStatus -> { + boolean processed = true; + ResourceModifiedMessage resourceModifiedMessage = null; + try { + + // delete the entry to lock the row to ensure unique processing + boolean wasDeleted = deletePersistedResourceModifiedMessage( + thePersistedResourceModifiedMessage.getPersistedResourceModifiedMessagePk()); + + Optional optionalResourceModifiedMessage = + inflatePersistedResourceMessage(thePersistedResourceModifiedMessage); + + if (wasDeleted && optionalResourceModifiedMessage.isPresent()) { + // the PK did exist and we were able to deleted it, ie, we are the only one processing the message + resourceModifiedMessage = optionalResourceModifiedMessage.get(); + submitResourceModified(resourceModifiedMessage); + } + + } catch (MessageDeliveryException exception) { + // we encountered an issue when trying to send the message so mark the transaction for rollback + ourLog.error( + "Channel submission failed for resource with id {} matching subscription with id {}. Further attempts will be performed at later time.", + resourceModifiedMessage.getPayloadId(), + resourceModifiedMessage.getSubscriptionId()); + processed = false; + theStatus.setRollbackOnly(); + } + + return processed; + }; + } + + private Optional inflatePersistedResourceMessage( + IPersistedResourceModifiedMessage thePersistedResourceModifiedMessage) { + ResourceModifiedMessage resourceModifiedMessage = null; + + try { + + resourceModifiedMessage = myResourceModifiedMessagePersistenceSvc.inflatePersistedResourceModifiedMessage( + thePersistedResourceModifiedMessage); + + } catch (ResourceNotFoundException e) { + IPersistedResourceModifiedMessagePK persistedResourceModifiedMessagePk = + thePersistedResourceModifiedMessage.getPersistedResourceModifiedMessagePk(); + + IdType idType = new IdType( + thePersistedResourceModifiedMessage.getResourceType(), + persistedResourceModifiedMessagePk.getResourcePid(), + persistedResourceModifiedMessagePk.getResourceVersion()); + + ourLog.warn( + "Scheduled submission will be ignored since resource {} cannot be found", idType.asStringValue()); + } + + return Optional.ofNullable(resourceModifiedMessage); + } + + private boolean deletePersistedResourceModifiedMessage(IPersistedResourceModifiedMessagePK theResourceModifiedPK) { + + try { + // delete the entry to lock the row to ensure unique processing + return myResourceModifiedMessagePersistenceSvc.deleteByPK(theResourceModifiedPK); + } catch (ResourceNotFoundException exception) { + ourLog.warn( + "thePersistedResourceModifiedMessage with {} and version {} could not be deleted as it may have already been deleted.", + theResourceModifiedPK.getResourcePid(), + theResourceModifiedPK.getResourceVersion()); + // we were not able to delete the pk. this implies that someone else did read/delete the PK and processed + // the message + // successfully before we did. + + return false; + } + } + + private ChannelProducerSettings getChannelProducerSettings() { + ChannelProducerSettings channelProducerSettings = new ChannelProducerSettings(); + channelProducerSettings.setQualifyChannelName(myStorageSettings.isQualifySubscriptionMatchingChannelName()); + return channelProducerSettings; + } + + public IChannelProducer getProcessingChannelForUnitTest() { + startIfNeeded(); + return (IChannelProducer) myMatchingChannel; + } +} diff --git a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.java b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.java index ebbfd974d38..6494f59a2e1 100644 --- a/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.java +++ b/hapi-fhir-jpaserver-subscription/src/main/java/ca/uhn/fhir/jpa/subscription/util/SubscriptionDebugLogInterceptor.java @@ -39,7 +39,7 @@ import java.util.function.Function; * This interceptor can be used for troubleshooting subscription processing. It provides very * detailed logging about the subscription processing pipeline. *

- * This interceptor loges each step in the processing pipeline with a + * This interceptor logs each step in the processing pipeline with a * different event code, using the event codes itemized in * {@link EventCodeEnum}. By default these are each placed in a logger with * a different name (e.g. ca.uhn.fhir.jpa.subscription.util.SubscriptionDebugLogInterceptor.SUBS20 @@ -91,7 +91,7 @@ public class SubscriptionDebugLogInterceptor { } log( EventCodeEnum.SUBS1, - "Resource {} was submitted to the processing pipeline (op={})", + "Resource {} is starting the processing pipeline (op={})", resourceId, theMessage.getOperationType()); } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptorTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptorTest.java deleted file mode 100644 index 889e2963160..00000000000 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionMatcherInterceptorTest.java +++ /dev/null @@ -1,60 +0,0 @@ -package ca.uhn.fhir.jpa.subscription.submit.interceptor; - -import ca.uhn.fhir.jpa.model.entity.StorageSettings; -import ca.uhn.fhir.jpa.subscription.channel.api.ChannelProducerSettings; -import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.Set; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hl7.fhir.dstu2.model.Subscription.SubscriptionChannelType.RESTHOOK; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - - -@ExtendWith(MockitoExtension.class) -public class SubscriptionMatcherInterceptorTest { - - @Mock - StorageSettings myStorageSettings; - @Mock - SubscriptionChannelFactory mySubscriptionChannelFactory; - @InjectMocks - SubscriptionMatcherInterceptor myUnitUnderTest; - @Captor - ArgumentCaptor myArgumentCaptor; - - @ParameterizedTest - @ValueSource(booleans = {false, true}) - public void testMethodStartIfNeeded_withQualifySubscriptionMatchingChannelNameProperty_mayQualifyChannelName(boolean theIsQualifySubMatchingChannelName){ - // given - boolean expectedResult = theIsQualifySubMatchingChannelName; - when(myStorageSettings.isQualifySubscriptionMatchingChannelName()).thenReturn(theIsQualifySubMatchingChannelName); - when(myStorageSettings.getSupportedSubscriptionTypes()).thenReturn(Set.of(RESTHOOK)); - - // when - myUnitUnderTest.startIfNeeded(); - - // then - ChannelProducerSettings capturedChannelProducerSettings = getCapturedChannelProducerSettings(); - assertThat(capturedChannelProducerSettings.isQualifyChannelName(), is(expectedResult)); - - } - - private ChannelProducerSettings getCapturedChannelProducerSettings(){ - verify(mySubscriptionChannelFactory).newMatchingSendingChannel(anyString(), myArgumentCaptor.capture()); - return myArgumentCaptor.getValue(); - } - - -} diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionSubmitInterceptorLoaderTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionSubmitInterceptorLoaderTest.java index 6ad42669f81..f5f11cbfb34 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionSubmitInterceptorLoaderTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/subscription/submit/interceptor/SubscriptionSubmitInterceptorLoaderTest.java @@ -6,12 +6,14 @@ import ca.uhn.fhir.interceptor.api.IInterceptorService; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.cache.IResourceVersionSvc; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; import ca.uhn.fhir.jpa.searchparam.config.SearchParamConfig; import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamProvider; import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory; import ca.uhn.fhir.jpa.subscription.submit.config.SubscriptionSubmitterConfig; +import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; import org.hl7.fhir.dstu2.model.Subscription; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -21,6 +23,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.PlatformTransactionManager; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; @@ -34,24 +37,12 @@ import static org.mockito.Mockito.verify; }) public class SubscriptionSubmitInterceptorLoaderTest { - @MockBean - private ISearchParamProvider mySearchParamProvider; - @MockBean - private IInterceptorService myInterceptorService; - @MockBean - private IValidationSupport myValidationSupport; - @MockBean - private SubscriptionChannelFactory mySubscriptionChannelFactory; - @MockBean - private DaoRegistry myDaoRegistry; @Autowired private SubscriptionSubmitInterceptorLoader mySubscriptionSubmitInterceptorLoader; @Autowired private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; @MockBean - private IResourceVersionSvc myResourceVersionSvc; - @MockBean - private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; + private IInterceptorService myInterceptorService; /** * It should be possible to run only the {@link SubscriptionSubmitterConfig} without the @@ -82,6 +73,25 @@ public class SubscriptionSubmitInterceptorLoaderTest { return storageSettings; } + @MockBean + private ISearchParamProvider mySearchParamProvider; + @MockBean + private IValidationSupport myValidationSupport; + @MockBean + private SubscriptionChannelFactory mySubscriptionChannelFactory; + @MockBean + private DaoRegistry myDaoRegistry; + @MockBean + private IResourceVersionSvc myResourceVersionSvc; + @MockBean + private IRequestPartitionHelperSvc myRequestPartitionHelperSvc; + @MockBean + private PlatformTransactionManager myPlatformTransactionManager; + @MockBean + private IResourceModifiedMessagePersistenceSvc myResourceModifiedMessagePersistenceSvc; + @MockBean + private IHapiTransactionService myHapiTransactionService; + } diff --git a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTriggerMatcherTest.java b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTriggerMatcherTest.java index ae47a026bb7..e06c969510d 100644 --- a/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTriggerMatcherTest.java +++ b/hapi-fhir-jpaserver-subscription/src/test/java/ca/uhn/fhir/jpa/topic/SubscriptionTriggerMatcherTest.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult; import ca.uhn.fhir.jpa.searchparam.matcher.SearchParamMatcher; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; +import ca.uhn.fhir.rest.api.server.RequestDetails; import org.hl7.fhir.r5.model.Encounter; import org.hl7.fhir.r5.model.IdType; import org.hl7.fhir.r5.model.SubscriptionTopic; diff --git a/hapi-fhir-jpaserver-test-dstu2/pom.xml b/hapi-fhir-jpaserver-test-dstu2/pom.xml index beab9e4ab2c..d6e6a1f94c3 100644 --- a/hapi-fhir-jpaserver-test-dstu2/pom.xml +++ b/hapi-fhir-jpaserver-test-dstu2/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/BaseSearchSvc.java b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/BaseSearchSvc.java index 37f52a21cf1..46f8fd95af4 100644 --- a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/BaseSearchSvc.java +++ b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/search/BaseSearchSvc.java @@ -9,6 +9,7 @@ import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.search.builder.SearchBuilder; +import ca.uhn.fhir.jpa.svc.MockHapiTransactionService; import ca.uhn.fhir.jpa.util.BaseIterator; import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.rest.api.server.storage.IResourcePersistentId; diff --git a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToStorageSettingsDstu2Test.java b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToStorageSettingsDstu2Test.java index d9b42cb7683..6f993478058 100644 --- a/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToStorageSettingsDstu2Test.java +++ b/hapi-fhir-jpaserver-test-dstu2/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestWithInterceptorRegisteredToStorageSettingsDstu2Test.java @@ -210,7 +210,6 @@ public class RestHookTestWithInterceptorRegisteredToStorageSettingsDstu2Test ext Subscription subscription1 = createSubscription(criteria1, payload, ourListenerServerBase); Subscription subscription2 = createSubscription(criteria2, payload, ourListenerServerBase); - runInTransaction(() -> { ourLog.info("All token indexes:\n * {}", myResourceIndexedSearchParamTokenDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * "))); }); diff --git a/hapi-fhir-jpaserver-test-dstu3/pom.xml b/hapi-fhir-jpaserver-test-dstu3/pom.xml index 01e61f2e7dc..d812bae0c9c 100644 --- a/hapi-fhir-jpaserver-test-dstu3/pom.xml +++ b/hapi-fhir-jpaserver-test-dstu3/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r4/pom.xml b/hapi-fhir-jpaserver-test-r4/pom.xml index 1626f6c4d6e..fa1a1f2432c 100644 --- a/hapi-fhir-jpaserver-test-r4/pom.xml +++ b/hapi-fhir-jpaserver-test-r4/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java index bdfae2d54fd..98fea30536f 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDaoTest.java @@ -16,8 +16,8 @@ import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.model.entity.ForcedId; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.partition.IRequestPartitionHelperSvc; -import ca.uhn.fhir.jpa.search.MockHapiTransactionService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.jpa.svc.MockHapiTransactionService; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index 13511df2607..826a7b382c3 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -25,7 +25,7 @@ import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.search.PersistedJpaSearchFirstPageBundleProvider; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.subscription.triggering.ISubscriptionTriggeringSvc; import ca.uhn.fhir.jpa.term.TermReadSvcImpl; import ca.uhn.fhir.jpa.util.SqlQuery; @@ -126,6 +126,7 @@ import static org.mockito.Mockito.when; @SuppressWarnings("JavadocBlankLines") @TestMethodOrder(MethodOrderer.MethodName.class) public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test { + @RegisterExtension @Order(0) public static final RestfulServerExtension ourServer = new RestfulServerExtension(FhirContext.forR4Cached()) @@ -139,7 +140,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test @Autowired private ISubscriptionTriggeringSvc mySubscriptionTriggeringSvc; @Autowired - private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; + private ResourceModifiedSubmitterSvc myResourceModifiedSubmitterSvc;; @Autowired private ReindexStep myReindexStep; @Autowired @@ -3090,7 +3091,7 @@ public class FhirResourceDaoR4QueryCountTest extends BaseResourceProviderR4Test // Setup myStorageSettings.addSupportedSubscriptionType(org.hl7.fhir.dstu2.model.Subscription.SubscriptionChannelType.RESTHOOK); - mySubscriptionMatcherInterceptor.startIfNeeded(); + myResourceModifiedSubmitterSvc.startIfNeeded(); for (int i = 0; i < 10; i++) { createPatient(withActiveTrue()); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4Test.java index 4cb7788307e..1b315cdca30 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4Test.java @@ -2,12 +2,14 @@ package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.dao.data.IResourceModifiedDao; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannel; -import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.test.util.SubscriptionTestUtil; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; +import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; import ca.uhn.fhir.test.utilities.server.HashMapResourceProviderExtension; import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; import ca.uhn.fhir.test.utilities.server.TransactionCapturingProviderExtension; @@ -61,7 +63,11 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test @Autowired protected SubscriptionTestUtil mySubscriptionTestUtil; @Autowired - protected SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; + protected ResourceModifiedSubmitterSvc myResourceModifiedSubmitterSvc; + @Autowired + protected IResourceModifiedMessagePersistenceSvc myResourceModifiedMessagePersistenceSvc; + @Autowired + protected IResourceModifiedDao myResourceModifiedDao; protected CountingInterceptor myCountingInterceptor; protected List mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); @Autowired @@ -84,6 +90,7 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test myStorageSettings.setAllowMultipleDelete(new JpaStorageSettings().isAllowMultipleDelete()); mySubscriptionTestUtil.unregisterSubscriptionInterceptor(); + myResourceModifiedDao.deleteAll(); } @BeforeEach @@ -102,7 +109,7 @@ public abstract class BaseSubscriptionsR4Test extends BaseResourceProviderR4Test waitForActivatedSubscriptionCount(0); } - LinkedBlockingChannel processingChannel = mySubscriptionMatcherInterceptor.getProcessingChannelForUnitTest(); + LinkedBlockingChannel processingChannel = (LinkedBlockingChannel) myResourceModifiedSubmitterSvc.getProcessingChannelForUnitTest(); if (processingChannel != null) { processingChannel.clearInterceptorsForUnitTest(); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/async/AsyncSubscriptionMessageSubmissionIT.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/async/AsyncSubscriptionMessageSubmissionIT.java new file mode 100644 index 00000000000..13a0b4b686c --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/async/AsyncSubscriptionMessageSubmissionIT.java @@ -0,0 +1,158 @@ +package ca.uhn.fhir.jpa.subscription.async; + +import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.subscription.BaseSubscriptionsR4Test; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.SynchronousSubscriptionMatcherInterceptor; +import ca.uhn.fhir.jpa.subscription.channel.api.ChannelConsumerSettings; +import ca.uhn.fhir.jpa.subscription.channel.api.IChannelReceiver; +import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory; +import ca.uhn.fhir.jpa.subscription.match.matcher.matching.IResourceModifiedConsumer; +import ca.uhn.fhir.jpa.subscription.message.TestQueueConsumerHandler; +import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedJsonMessage; +import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; +import ca.uhn.fhir.jpa.test.util.StoppableSubscriptionDeliveringRestHookSubscriber; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Subscription; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.test.context.ContextConfiguration; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@ContextConfiguration(classes = {AsyncSubscriptionMessageSubmissionIT.SpringConfig.class}) +public class AsyncSubscriptionMessageSubmissionIT extends BaseSubscriptionsR4Test { + + @SpyBean + IResourceModifiedConsumer myResourceModifiedConsumer; + + @Autowired + AsyncResourceModifiedSubmitterSvc myAsyncResourceModifiedSubmitterSvc; + + @Autowired + private SubscriptionChannelFactory myChannelFactory; + + @Autowired SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; + + @Autowired + StoppableSubscriptionDeliveringRestHookSubscriber myStoppableSubscriptionDeliveringRestHookSubscriber; + private TestQueueConsumerHandler myQueueConsumerHandler; + + @AfterEach + public void cleanupStoppableSubscriptionDeliveringRestHookSubscriber() { + myStoppableSubscriptionDeliveringRestHookSubscriber.setCountDownLatch(null); + myStoppableSubscriptionDeliveringRestHookSubscriber.unPause(); + myStorageSettings.setTriggerSubscriptionsForNonVersioningChanges(new JpaStorageSettings().isTriggerSubscriptionsForNonVersioningChanges()); + myStorageSettings.setTagStorageMode(new JpaStorageSettings().getTagStorageMode()); + } + + @BeforeEach + public void beforeRegisterRestHookListenerAndSchedulePoisonPillInterceptor() { + mySubscriptionTestUtil.registerMessageInterceptor(); + + IChannelReceiver receiver = myChannelFactory.newMatchingReceivingChannel("my-queue-name", new ChannelConsumerSettings()); + myQueueConsumerHandler = new TestQueueConsumerHandler(); + receiver.subscribe(myQueueConsumerHandler); + + myStorageSettings.setTagStorageMode(JpaStorageSettings.TagStorageModeEnum.NON_VERSIONED); + } + + @Test + public void testSpringInjects_BeanOfTypeSubscriptionMatchingInterceptor_whenBeanDeclarationIsOverwrittenLocally(){ + assertFalse(mySubscriptionMatcherInterceptor instanceof SynchronousSubscriptionMatcherInterceptor); + } + + @Test + // the purpose of this test is to assert that a resource matching a given subscription is + // delivered asynchronously to the subscription processing pipeline. + public void testAsynchronousDeliveryOfResourceMatchingASubscription_willSucceed() throws Exception { + String aCode = "zoop"; + String aSystem = "SNOMED-CT"; + // given + createAndSubmitSubscriptionWithCriteria("[Observation]"); + waitForActivatedSubscriptionCount(1); + + // when + Observation obs = sendObservation(aCode, aSystem); + + assertCountOfResourcesNeedingSubmission(2); // the subscription and the observation + assertCountOfResourcesReceivedAtSubscriptionTerminalEndpoint(0); + + // since scheduled tasks are disabled during tests, let's trigger a submission + // just like the AsyncResourceModifiedProcessingSchedulerSvc would. + myAsyncResourceModifiedSubmitterSvc.runDeliveryPass(); + + //then + waitForQueueToDrain(); + assertCountOfResourcesNeedingSubmission(0); + assertCountOfResourcesReceivedAtSubscriptionTerminalEndpoint(1); + + Observation observation = (Observation) fetchSingleResourceFromSubscriptionTerminalEndpoint(); + Coding coding = observation.getCode().getCodingFirstRep(); + + assertThat(coding.getCode(), equalTo(aCode)); + assertThat(coding.getSystem(), equalTo(aSystem)); + + } + + private void assertCountOfResourcesNeedingSubmission(int theExpectedCount) { + assertThat(myResourceModifiedMessagePersistenceSvc.findAllOrderedByCreatedTime(), hasSize(theExpectedCount)); + } + + private Subscription createAndSubmitSubscriptionWithCriteria(String theCriteria) { + Subscription subscription = new Subscription(); + subscription.setReason("Monitor new neonatal function (note, age will be determined by the monitor)"); + subscription.setStatus(Subscription.SubscriptionStatus.REQUESTED); + subscription.setCriteria(theCriteria); + + Subscription.SubscriptionChannelComponent channel = subscription.getChannel(); + channel.setType(Subscription.SubscriptionChannelType.MESSAGE); + channel.setPayload("application/fhir+json"); + channel.setEndpoint("channel:my-queue-name"); + + subscription.setChannel(channel); + postOrPutSubscription(subscription); + + myAsyncResourceModifiedSubmitterSvc.runDeliveryPass(); + + return subscription; + } + + + private IBaseResource fetchSingleResourceFromSubscriptionTerminalEndpoint() { + assertThat(myQueueConsumerHandler.getMessages().size(), is(equalTo(1))); + ResourceModifiedJsonMessage resourceModifiedJsonMessage = myQueueConsumerHandler.getMessages().get(0); + ResourceModifiedMessage payload = resourceModifiedJsonMessage.getPayload(); + String payloadString = payload.getPayloadString(); + IBaseResource resource = myFhirContext.newJsonParser().parseResource(payloadString); + myQueueConsumerHandler.clearMessages(); + return resource; + } + + private void assertCountOfResourcesReceivedAtSubscriptionTerminalEndpoint(int expectedCount) { + assertThat(myQueueConsumerHandler.getMessages(), hasSize(expectedCount)); + } + + @Configuration + public static class SpringConfig { + + @Primary + @Bean + public SubscriptionMatcherInterceptor subscriptionMatcherInterceptor() { + return new SubscriptionMatcherInterceptor(); + } + } + +} diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/message/MessageSubscriptionR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/message/MessageSubscriptionR4Test.java index 2e9b8f84edb..540f5ae1f7d 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/message/MessageSubscriptionR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/message/MessageSubscriptionR4Test.java @@ -1,6 +1,11 @@ package ca.uhn.fhir.jpa.subscription.message; +import ca.uhn.fhir.interceptor.model.RequestPartitionId; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.dao.data.IResourceModifiedDao; +import ca.uhn.fhir.jpa.model.entity.IPersistedResourceModifiedMessage; +import ca.uhn.fhir.jpa.model.entity.IPersistedResourceModifiedMessagePK; +import ca.uhn.fhir.jpa.model.entity.PersistedResourceModifiedMessageEntityPK; import ca.uhn.fhir.jpa.subscription.BaseSubscriptionsR4Test; import ca.uhn.fhir.jpa.subscription.channel.api.ChannelConsumerSettings; import ca.uhn.fhir.jpa.subscription.channel.api.IChannelReceiver; @@ -11,20 +16,28 @@ import ca.uhn.fhir.jpa.test.util.StoppableSubscriptionDeliveringRestHookSubscrib import ca.uhn.fhir.rest.client.api.Header; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.interceptor.AdditionalRequestHeadersInterceptor; +import ca.uhn.fhir.rest.server.messaging.BaseResourceMessage; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Subscription; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; import java.util.List; import java.util.stream.Collectors; @@ -49,6 +62,12 @@ public class MessageSubscriptionR4Test extends BaseSubscriptionsR4Test { private static final Logger ourLog = LoggerFactory.getLogger(MessageSubscriptionR4Test.class); private TestQueueConsumerHandler handler; + @Autowired + IResourceModifiedDao myResourceModifiedDao; + + @Autowired + private PlatformTransactionManager myTxManager; + @Autowired StoppableSubscriptionDeliveringRestHookSubscriber myStoppableSubscriptionDeliveringRestHookSubscriber; @@ -176,6 +195,109 @@ public class MessageSubscriptionR4Test extends BaseSubscriptionsR4Test { } + @Test + public void testMethodFindAllOrdered_willReturnAllPersistedResourceModifiedMessagesOrderedByCreatedTime(){ + mySubscriptionTestUtil.unregisterSubscriptionInterceptor(); + + // given + Patient patient = sendPatient(); + Organization organization = sendOrganization(); + + ResourceModifiedMessage patientResourceModifiedMessage = new ResourceModifiedMessage(myFhirContext, patient, BaseResourceMessage.OperationTypeEnum.CREATE); + ResourceModifiedMessage organizationResourceModifiedMessage = new ResourceModifiedMessage(myFhirContext, organization, BaseResourceMessage.OperationTypeEnum.CREATE); + + IPersistedResourceModifiedMessage patientPersistedMessage = myResourceModifiedMessagePersistenceSvc.persist(patientResourceModifiedMessage); + IPersistedResourceModifiedMessage organizationPersistedMessage = myResourceModifiedMessagePersistenceSvc.persist(organizationResourceModifiedMessage); + + // when + List allPersisted = myResourceModifiedMessagePersistenceSvc.findAllOrderedByCreatedTime(); + + // then + assertOnPksAndOrder(allPersisted, List.of(patientPersistedMessage, organizationPersistedMessage)); + + } + + @Test + public void testMethodDeleteByPK_whenEntityExists_willDeleteTheEntityAndReturnTrue(){ + mySubscriptionTestUtil.unregisterSubscriptionInterceptor(); + + // given + TransactionTemplate transactionTemplate = new TransactionTemplate(myTxManager); + Patient patient = sendPatient(); + + ResourceModifiedMessage patientResourceModifiedMessage = new ResourceModifiedMessage(myFhirContext, patient, BaseResourceMessage.OperationTypeEnum.CREATE); + IPersistedResourceModifiedMessage persistedResourceModifiedMessage = myResourceModifiedMessagePersistenceSvc.persist(patientResourceModifiedMessage); + + // when + boolean wasDeleted = transactionTemplate.execute(tx -> myResourceModifiedMessagePersistenceSvc.deleteByPK(persistedResourceModifiedMessage.getPersistedResourceModifiedMessagePk())); + + // then + assertThat(wasDeleted, is(Boolean.TRUE)); + assertThat(myResourceModifiedMessagePersistenceSvc.findAllOrderedByCreatedTime(), hasSize(0)); + } + + @Test + public void testMethodDeleteByPK_whenEntityDoesNotExist_willReturnFalse(){ + mySubscriptionTestUtil.unregisterSubscriptionInterceptor(); + + // given + TransactionTemplate transactionTemplate = new TransactionTemplate(myTxManager); + IPersistedResourceModifiedMessagePK nonExistentResourceWithPk = PersistedResourceModifiedMessageEntityPK.with("one", "one"); + + // when + boolean wasDeleted = transactionTemplate.execute(tx -> myResourceModifiedMessagePersistenceSvc.deleteByPK(nonExistentResourceWithPk)); + + // then + assertThat(wasDeleted, is(Boolean.FALSE)); + } + + @Test + public void testPersistedResourceModifiedMessage_whenFetchFromDb_willEqualOriginalMessage() throws JsonProcessingException { + mySubscriptionTestUtil.unregisterSubscriptionInterceptor(); + // given + TransactionTemplate transactionTemplate = new TransactionTemplate(myTxManager); + Observation obs = sendObservation("zoop", "SNOMED-CT", "theExplicitSource", "theRequestId"); + + ResourceModifiedMessage originalResourceModifiedMessage = createResourceModifiedMessage(obs); + + transactionTemplate.execute(tx -> { + + IPersistedResourceModifiedMessage persistedResourceModifiedMessage = myResourceModifiedMessagePersistenceSvc.persist(originalResourceModifiedMessage); + + // when + ResourceModifiedMessage restoredResourceModifiedMessage = myResourceModifiedMessagePersistenceSvc.inflatePersistedResourceModifiedMessage(persistedResourceModifiedMessage); + + // then + assertEquals(toJson(originalResourceModifiedMessage), toJson(restoredResourceModifiedMessage)); + assertEquals(originalResourceModifiedMessage, restoredResourceModifiedMessage); + + return null; + }); + + } + + private ResourceModifiedMessage createResourceModifiedMessage(Observation theObservation){ + ResourceModifiedMessage retVal = new ResourceModifiedMessage(myFhirContext, theObservation, BaseResourceMessage.OperationTypeEnum.CREATE); + retVal.setSubscriptionId("subId"); + retVal.setTransactionId("txId"); + retVal.setMessageKey("messageKey"); + retVal.setMediaType("json"); + retVal.setAttribute("attKey", "attValue"); + retVal.setPartitionId(RequestPartitionId.allPartitions()); + return retVal; + } + + private static void assertEquals(ResourceModifiedMessage theMsg, ResourceModifiedMessage theComparedTo){ + assertThat(theMsg.getPayloadId(), equalTo(theComparedTo.getPayloadId())); + assertThat(theMsg.getOperationType(), equalTo(theComparedTo.getOperationType())); + assertThat(theMsg.getPayloadString(), equalTo(theComparedTo.getPayloadString())); + assertThat(theMsg.getSubscriptionId(), equalTo(theComparedTo.getSubscriptionId())); + assertThat(theMsg.getMediaType(), equalTo(theComparedTo.getMediaType())); + assertThat(theMsg.getMessageKeyOrNull(), equalTo(theComparedTo.getMessageKeyOrNull())); + assertThat(theMsg.getTransactionId(), equalTo(theComparedTo.getTransactionId())); + assertThat(theMsg.getAttributes(), equalTo(theComparedTo.getAttributes())); + } + private void maybeAddHeaderInterceptor(IGenericClient theClient, List

theHeaders) { if(theHeaders.isEmpty()){ return; @@ -215,4 +337,32 @@ public class MessageSubscriptionR4Test extends BaseSubscriptionsR4Test { return (T) resource; } + private static void assertEquals(String theMsg, String theComparedTo){ + assertThat(theMsg, equalTo(theComparedTo)); + } + + private static String toJson(Object theRequest) { + try { + return new ObjectMapper().writer().writeValueAsString(theRequest); + } catch (JsonProcessingException theE) { + throw new AssertionError("Failure during serialization: " + theE); + } + } + + private static void assertOnPksAndOrder(List theFetchedResourceModifiedMessageList, List theCompareToList ){ + assertThat(theFetchedResourceModifiedMessageList, hasSize(theCompareToList.size())); + + List fetchedPks = theFetchedResourceModifiedMessageList + .stream() + .map(IPersistedResourceModifiedMessage::getPersistedResourceModifiedMessagePk) + .collect(Collectors.toList()); + + List compareToPks = theCompareToList + .stream() + .map(IPersistedResourceModifiedMessage::getPersistedResourceModifiedMessagePk) + .collect(Collectors.toList()); + + Assertions.assertEquals(fetchedPks, compareToPks); + + } } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookActivatesPreExistingSubscriptionsR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookActivatesPreExistingSubscriptionsR4Test.java index 30e0e1ef63b..b4f22254adb 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookActivatesPreExistingSubscriptionsR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookActivatesPreExistingSubscriptionsR4Test.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.subscription.resthook; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; -import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.test.util.SubscriptionTestUtil; import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.Update; @@ -52,7 +52,7 @@ public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourc @Autowired private SubscriptionTestUtil mySubscriptionTestUtil; @Autowired - private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; + private ResourceModifiedSubmitterSvc myResourceModifiedSubmitterSvc; @AfterEach public void afterUnregisterRestHookListener() { @@ -63,7 +63,7 @@ public class RestHookActivatesPreExistingSubscriptionsR4Test extends BaseResourc @BeforeEach public void beforeSetSubscriptionActivatingInterceptor() { myStorageSettings.addSupportedSubscriptionType(org.hl7.fhir.dstu2.model.Subscription.SubscriptionChannelType.RESTHOOK); - mySubscriptionMatcherInterceptor.startIfNeeded(); + myResourceModifiedSubmitterSvc.startIfNeeded(); mySubscriptionLoader.doSyncSubscriptionsForUnitTest(); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java index 68152e7f1a2..ace7684317f 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/resthook/RestHookTestR4Test.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.subscription.resthook; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.subscription.BaseSubscriptionsR4Test; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.test.util.StoppableSubscriptionDeliveringRestHookSubscriber; import ca.uhn.fhir.jpa.topic.SubscriptionTopicDispatcher; import ca.uhn.fhir.jpa.topic.SubscriptionTopicRegistry; @@ -31,6 +32,7 @@ import org.hl7.fhir.r4.model.SearchParameter; import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.Subscription; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -64,6 +66,9 @@ import static org.junit.jupiter.api.Assertions.fail; public class RestHookTestR4Test extends BaseSubscriptionsR4Test { private static final Logger ourLog = LoggerFactory.getLogger(RestHookTestR4Test.class); + @Autowired + ResourceModifiedSubmitterSvc myResourceModifiedSubmitterSvc; + @Autowired StoppableSubscriptionDeliveringRestHookSubscriber myStoppableSubscriptionDeliveringRestHookSubscriber; @Autowired(required = false) @@ -113,7 +118,6 @@ public class RestHookTestR4Test extends BaseSubscriptionsR4Test { assertEquals("IN_MEMORY", subscription.getMeta().getTag().get(0).getCode()); } - @Test public void testRestHookSubscriptionApplicationFhirJson() throws Exception { String payload = "application/fhir+json"; diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/svc/ResourceModifiedSubmitterSvcTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/svc/ResourceModifiedSubmitterSvcTest.java new file mode 100644 index 00000000000..872ec955c81 --- /dev/null +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/subscription/svc/ResourceModifiedSubmitterSvcTest.java @@ -0,0 +1,141 @@ +package ca.uhn.fhir.jpa.subscription.svc; + +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; +import ca.uhn.fhir.jpa.model.entity.ResourceModifiedEntity; +import ca.uhn.fhir.jpa.model.entity.StorageSettings; +import ca.uhn.fhir.jpa.subscription.channel.api.ChannelProducerSettings; +import ca.uhn.fhir.jpa.subscription.channel.api.IChannelProducer; +import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory; +import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; +import ca.uhn.fhir.jpa.svc.MockHapiTransactionService; +import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.messaging.MessageDeliveryException; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.SimpleTransactionStatus; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ResourceModifiedSubmitterSvcTest { + + @Mock + StorageSettings myStorageSettings; + @Mock + SubscriptionChannelFactory mySubscriptionChannelFactory; + @Mock + IResourceModifiedMessagePersistenceSvc myResourceModifiedMessagePersistenceSvc; + @Captor + ArgumentCaptor myArgumentCaptor; + @Mock + IChannelProducer myChannelProducer; + + ResourceModifiedSubmitterSvc myResourceModifiedSubmitterSvc; + TransactionStatus myCapturingTransactionStatus; + + @BeforeEach + public void beforeEach(){ + myCapturingTransactionStatus = new SimpleTransactionStatus(); + lenient().when(myStorageSettings.hasSupportedSubscriptionTypes()).thenReturn(true); + lenient().when(mySubscriptionChannelFactory.newMatchingSendingChannel(anyString(), any())).thenReturn(myChannelProducer); + + IHapiTransactionService hapiTransactionService = new MockHapiTransactionService(myCapturingTransactionStatus); + myResourceModifiedSubmitterSvc = new ResourceModifiedSubmitterSvc( + myStorageSettings, + mySubscriptionChannelFactory, + myResourceModifiedMessagePersistenceSvc, + hapiTransactionService); + + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void testMethodStartIfNeeded_withQualifySubscriptionMatchingChannelNameProperty_mayQualifyChannelName(boolean theIsQualifySubMatchingChannelName){ + // given + boolean expectedResult = theIsQualifySubMatchingChannelName; + when(myStorageSettings.isQualifySubscriptionMatchingChannelName()).thenReturn(theIsQualifySubMatchingChannelName); + + // when + myResourceModifiedSubmitterSvc.startIfNeeded(); + + // then + ChannelProducerSettings capturedChannelProducerSettings = getCapturedChannelProducerSettings(); + assertThat(capturedChannelProducerSettings.isQualifyChannelName(), is(expectedResult)); + + } + + @Test + public void testSubmitPersisedResourceModifiedMessage_withExistingPersistedResourceModifiedMessage_willSucceed(){ + // given + // a successful deletion implies that the message did exist. + when(myResourceModifiedMessagePersistenceSvc.deleteByPK(any())).thenReturn(true); + when(myResourceModifiedMessagePersistenceSvc.inflatePersistedResourceModifiedMessage(any())).thenReturn(new ResourceModifiedMessage()); + + // when + boolean wasProcessed = myResourceModifiedSubmitterSvc.submitPersisedResourceModifiedMessage(new ResourceModifiedEntity()); + + // then + assertThat(wasProcessed, is(Boolean.TRUE)); + assertThat(myCapturingTransactionStatus.isRollbackOnly(), is(Boolean.FALSE)); + verify(myChannelProducer, times(1)).send(any()); + + } + + @Test + public void testSubmitPersisedResourceModifiedMessage_whenMessageWasAlreadyProcess_willSucceed(){ + // given + // deletion fails, someone else was faster and processed the message + when(myResourceModifiedMessagePersistenceSvc.deleteByPK(any())).thenReturn(false); + when(myResourceModifiedMessagePersistenceSvc.inflatePersistedResourceModifiedMessage(any())).thenReturn(new ResourceModifiedMessage()); + + // when + boolean wasProcessed = myResourceModifiedSubmitterSvc.submitPersisedResourceModifiedMessage(new ResourceModifiedEntity()); + + // then + assertThat(wasProcessed, is(Boolean.TRUE)); + assertThat(myCapturingTransactionStatus.isRollbackOnly(), is(Boolean.FALSE)); + // we do not send a message which was already sent + verify(myChannelProducer, times(0)).send(any()); + + } + + @Test + public void testSubmitPersisedResourceModifiedMessage_whitErrorOnSending_willRollbackDeletion(){ + // given + when(myResourceModifiedMessagePersistenceSvc.deleteByPK(any())).thenReturn(true); + when(myResourceModifiedMessagePersistenceSvc.inflatePersistedResourceModifiedMessage(any())).thenReturn(new ResourceModifiedMessage()); + + // simulate failure writing to the channel + when(myChannelProducer.send(any())).thenThrow(new MessageDeliveryException("sendingError")); + + // when + boolean wasProcessed = myResourceModifiedSubmitterSvc.submitPersisedResourceModifiedMessage(new ResourceModifiedEntity()); + + // then + assertThat(wasProcessed, is(Boolean.FALSE)); + assertThat(myCapturingTransactionStatus.isRollbackOnly(), is(Boolean.TRUE)); + + } + + private ChannelProducerSettings getCapturedChannelProducerSettings(){ + verify(mySubscriptionChannelFactory).newMatchingSendingChannel(anyString(), myArgumentCaptor.capture()); + return myArgumentCaptor.getValue(); + } + +} diff --git a/hapi-fhir-jpaserver-test-r4b/pom.xml b/hapi-fhir-jpaserver-test-r4b/pom.xml index b1bb1131bb2..2fefb661a04 100644 --- a/hapi-fhir-jpaserver-test-r4b/pom.xml +++ b/hapi-fhir-jpaserver-test-r4b/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4BTest.java b/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4BTest.java index 9dd1197fce1..bd551395c22 100644 --- a/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4BTest.java +++ b/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR4BTest.java @@ -4,7 +4,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.provider.r4b.BaseResourceProviderR4BTest; import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannel; -import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.test.util.SubscriptionTestUtil; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.server.SystemRequestDetails; @@ -63,7 +63,7 @@ public abstract class BaseSubscriptionsR4BTest extends BaseResourceProviderR4BTe @Autowired protected SubscriptionTestUtil mySubscriptionTestUtil; @Autowired - protected SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; + protected ResourceModifiedSubmitterSvc myResourceModifiedSubmitterSvc; protected CountingInterceptor myCountingInterceptor; protected List mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); @Autowired @@ -104,12 +104,12 @@ public abstract class BaseSubscriptionsR4BTest extends BaseResourceProviderR4BTe waitForActivatedSubscriptionCount(0); } - LinkedBlockingChannel processingChannel = mySubscriptionMatcherInterceptor.getProcessingChannelForUnitTest(); + myCountingInterceptor = new CountingInterceptor(); + + LinkedBlockingChannel processingChannel = (LinkedBlockingChannel) myResourceModifiedSubmitterSvc.getProcessingChannelForUnitTest(); + if (processingChannel != null) { processingChannel.clearInterceptorsForUnitTest(); - } - myCountingInterceptor = new CountingInterceptor(); - if (processingChannel != null) { processingChannel.addInterceptor(myCountingInterceptor); } } diff --git a/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestR4BTest.java b/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestR4BTest.java index 2537f195549..2e8761c363a 100644 --- a/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestR4BTest.java +++ b/hapi-fhir-jpaserver-test-r4b/src/test/java/ca/uhn/fhir/jpa/subscription/RestHookTestR4BTest.java @@ -2,6 +2,7 @@ package ca.uhn.fhir.jpa.subscription; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.test.util.StoppableSubscriptionDeliveringRestHookSubscriber; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.CacheControlDirective; @@ -25,6 +26,7 @@ import org.hl7.fhir.r4b.model.SearchParameter; import org.hl7.fhir.r4b.model.StringType; import org.hl7.fhir.r4b.model.Subscription; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -56,6 +58,9 @@ import static org.junit.jupiter.api.Assertions.fail; public class RestHookTestR4BTest extends BaseSubscriptionsR4BTest { private static final Logger ourLog = LoggerFactory.getLogger(RestHookTestR4BTest.class); + @Autowired + ResourceModifiedSubmitterSvc myResourceModifiedSubmitterSvc; + @Autowired StoppableSubscriptionDeliveringRestHookSubscriber myStoppableSubscriptionDeliveringRestHookSubscriber; diff --git a/hapi-fhir-jpaserver-test-r5/pom.xml b/hapi-fhir-jpaserver-test-r5/pom.xml index 42100203521..834edcf5768 100644 --- a/hapi-fhir-jpaserver-test-r5/pom.xml +++ b/hapi-fhir-jpaserver-test-r5/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR5Test.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR5Test.java index f372bee9ec1..a39f187b5ae 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR5Test.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/BaseSubscriptionsR5Test.java @@ -12,7 +12,7 @@ import ca.uhn.fhir.jpa.subscription.channel.impl.LinkedBlockingChannel; import ca.uhn.fhir.jpa.subscription.model.CanonicalSubscriptionChannelType; import ca.uhn.fhir.jpa.subscription.model.CanonicalTopicSubscriptionFilter; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; -import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.test.util.SubscriptionTestUtil; import ca.uhn.fhir.jpa.topic.SubscriptionTopicLoader; import ca.uhn.fhir.jpa.topic.SubscriptionTopicRegistry; @@ -73,7 +73,7 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test @Autowired protected SubscriptionTestUtil mySubscriptionTestUtil; @Autowired - protected SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; + protected ResourceModifiedSubmitterSvc myResourceModifiedSubmitterSvc; protected CountingInterceptor myCountingInterceptor; protected List mySubscriptionIds = Collections.synchronizedList(new ArrayList<>()); @Autowired @@ -110,7 +110,7 @@ public abstract class BaseSubscriptionsR5Test extends BaseResourceProviderR5Test waitForActivatedSubscriptionCount(0); } - LinkedBlockingChannel processingChannel = mySubscriptionMatcherInterceptor.getProcessingChannelForUnitTest(); + LinkedBlockingChannel processingChannel = (LinkedBlockingChannel) myResourceModifiedSubmitterSvc.getProcessingChannelForUnitTest(); if (processingChannel != null) { processingChannel.clearInterceptorsForUnitTest(); } diff --git a/hapi-fhir-jpaserver-test-utilities/pom.xml b/hapi-fhir-jpaserver-test-utilities/pom.xml index ce84de56ef4..d774771251e 100644 --- a/hapi-fhir-jpaserver-test-utilities/pom.xml +++ b/hapi-fhir-jpaserver-test-utilities/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/search/MockHapiTransactionService.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/svc/MockHapiTransactionService.java similarity index 73% rename from hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/search/MockHapiTransactionService.java rename to hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/svc/MockHapiTransactionService.java index 76a080e4ca1..29c2eae81ea 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/search/MockHapiTransactionService.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/svc/MockHapiTransactionService.java @@ -17,9 +17,10 @@ * limitations under the License. * #L% */ -package ca.uhn.fhir.jpa.search; +package ca.uhn.fhir.jpa.svc; import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; +import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.SimpleTransactionStatus; import org.springframework.transaction.support.TransactionCallback; @@ -27,9 +28,19 @@ import javax.annotation.Nullable; public class MockHapiTransactionService extends HapiTransactionService { + private TransactionStatus myTransactionStatus; + + public MockHapiTransactionService() { + this(new SimpleTransactionStatus()); + } + + public MockHapiTransactionService(TransactionStatus theTransactionStatus) { + myTransactionStatus = theTransactionStatus; + } + @Nullable @Override protected T doExecute(ExecutionBuilder theExecutionBuilder, TransactionCallback theCallback) { - return theCallback.doInTransaction(new SimpleTransactionStatus()); + return theCallback.doInTransaction(myTransactionStatus); } } diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestDstu2Config.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestDstu2Config.java index 6d05e2e500d..ad9e8ebf4ce 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestDstu2Config.java @@ -25,10 +25,16 @@ import ca.uhn.fhir.jpa.batch2.JpaBatch2Config; import ca.uhn.fhir.jpa.config.HapiJpaConfig; import ca.uhn.fhir.jpa.config.JpaDstu2Config; import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect; +import ca.uhn.fhir.jpa.model.entity.StorageSettings; +import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory; +import ca.uhn.fhir.jpa.subscription.match.matcher.matching.IResourceModifiedConsumer; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener; import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; +import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; import ca.uhn.fhir.system.HapiTestSystemProperties; import ca.uhn.fhir.validation.IInstanceValidatorModule; import ca.uhn.fhir.validation.ResultSeverityEnum; @@ -205,4 +211,5 @@ public class TestDstu2Config { return requestValidator; } + } diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestDstu3Config.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestDstu3Config.java index a7d2881eb7e..08515a33a31 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestDstu3Config.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestDstu3Config.java @@ -26,15 +26,21 @@ import ca.uhn.fhir.jpa.config.HapiJpaConfig; import ca.uhn.fhir.jpa.config.PackageLoaderConfig; import ca.uhn.fhir.jpa.config.dstu3.JpaDstu3Config; import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect; +import ca.uhn.fhir.jpa.model.entity.StorageSettings; +import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory; import ca.uhn.fhir.jpa.subscription.match.deliver.email.EmailSenderImpl; import ca.uhn.fhir.jpa.subscription.match.deliver.email.IEmailSender; +import ca.uhn.fhir.jpa.subscription.match.matcher.matching.IResourceModifiedConsumer; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener; import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; import ca.uhn.fhir.rest.server.mail.IMailSvc; import ca.uhn.fhir.rest.server.mail.MailConfig; import ca.uhn.fhir.rest.server.mail.MailSvc; +import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; import ca.uhn.fhir.system.HapiTestSystemProperties; import ca.uhn.fhir.validation.ResultSeverityEnum; import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder; @@ -220,5 +226,4 @@ public class TestDstu3Config { return new PropertySourcesPlaceholderConfigurer(); } - } diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR4BConfig.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR4BConfig.java index 5beaab359db..e6473f15647 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR4BConfig.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR4BConfig.java @@ -27,11 +27,17 @@ import ca.uhn.fhir.jpa.binstore.MemoryBinaryStorageSvcImpl; import ca.uhn.fhir.jpa.config.HapiJpaConfig; import ca.uhn.fhir.jpa.config.r4b.JpaR4BConfig; import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect; +import ca.uhn.fhir.jpa.model.entity.StorageSettings; +import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory; +import ca.uhn.fhir.jpa.subscription.match.matcher.matching.IResourceModifiedConsumer; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.topic.SubscriptionTopicConfig; import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener; import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; +import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; import ca.uhn.fhir.system.HapiTestSystemProperties; import ca.uhn.fhir.validation.ResultSeverityEnum; import net.ttddyy.dsproxy.listener.SingleQueryCountHolder; diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR4Config.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR4Config.java index b88ec150dd6..a95f721fa83 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestR4Config.java @@ -28,12 +28,18 @@ import ca.uhn.fhir.jpa.config.HapiJpaConfig; import ca.uhn.fhir.jpa.config.PackageLoaderConfig; import ca.uhn.fhir.jpa.config.r4.JpaR4Config; import ca.uhn.fhir.jpa.config.util.HapiEntityManagerFactoryUtil; +import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; import ca.uhn.fhir.jpa.model.dialect.HapiFhirH2Dialect; +import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.jpa.searchparam.config.NicknameServiceConfig; +import ca.uhn.fhir.jpa.subscription.channel.subscription.SubscriptionChannelFactory; +import ca.uhn.fhir.jpa.subscription.match.matcher.matching.IResourceModifiedConsumer; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.util.CircularQueueCaptureQueriesListener; import ca.uhn.fhir.jpa.util.CurrentThreadCaptureQueriesListener; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.server.interceptor.RequestValidatingInterceptor; +import ca.uhn.fhir.subscription.api.IResourceModifiedMessagePersistenceSvc; import ca.uhn.fhir.system.HapiTestSystemProperties; import ca.uhn.fhir.validation.ResultSeverityEnum; import net.ttddyy.dsproxy.listener.SingleQueryCountHolder; diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestSubscriptionMatcherInterceptorConfig.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestSubscriptionMatcherInterceptorConfig.java new file mode 100644 index 00000000000..6734aec48c8 --- /dev/null +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestSubscriptionMatcherInterceptorConfig.java @@ -0,0 +1,48 @@ +/*- + * #%L + * HAPI FHIR JPA Server Test Utilities + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package ca.uhn.fhir.jpa.test.config; + +import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; +import ca.uhn.fhir.jpa.subscription.submit.interceptor.SynchronousSubscriptionMatcherInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * Production environments submit modified resources to the subscription processing pipeline asynchronously, ie, a + * modified resource is 'planned' for submission which is performed at a later time by a scheduled task. + * + * The purpose of this class is to provide submission of modified resources during tests since task scheduling required + * for asynchronous submission are either disabled or not present in testing context. + * + * Careful consideration is advised when configuring test context as the SubscriptionMatcherInterceptor Bean instantiated + * below will overwrite the Bean provided by class SubscriptionMatcherInterceptorConfig if both configuration classes + * are present in the context. + */ +@Configuration +public class TestSubscriptionMatcherInterceptorConfig { + + @Primary + @Bean + public SubscriptionMatcherInterceptor subscriptionMatcherInterceptor() { + return new SynchronousSubscriptionMatcherInterceptor(); + } + +} diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/util/SubscriptionTestUtil.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/util/SubscriptionTestUtil.java index 27f95748b09..55ac4b30458 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/util/SubscriptionTestUtil.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/util/SubscriptionTestUtil.java @@ -29,8 +29,8 @@ import ca.uhn.fhir.jpa.subscription.match.deliver.email.EmailSenderImpl; import ca.uhn.fhir.jpa.subscription.match.deliver.email.SubscriptionDeliveringEmailSubscriber; import ca.uhn.fhir.jpa.subscription.match.registry.ActiveSubscription; import ca.uhn.fhir.jpa.subscription.match.registry.SubscriptionRegistry; -import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionMatcherInterceptor; import ca.uhn.fhir.jpa.subscription.submit.interceptor.SubscriptionSubmitInterceptorLoader; +import ca.uhn.fhir.jpa.subscription.submit.svc.ResourceModifiedSubmitterSvc; import ca.uhn.fhir.jpa.subscription.util.SubscriptionDebugLogInterceptor; import org.hl7.fhir.dstu2.model.Subscription; import org.hl7.fhir.instance.model.api.IIdType; @@ -45,7 +45,7 @@ public class SubscriptionTestUtil { @Autowired private SubscriptionSubmitInterceptorLoader mySubscriptionSubmitInterceptorLoader; @Autowired - private SubscriptionMatcherInterceptor mySubscriptionMatcherInterceptor; + private ResourceModifiedSubmitterSvc myResourceModifiedSubmitterSvc; @Autowired private SubscriptionRegistry mySubscriptionRegistry; @Autowired @@ -56,7 +56,7 @@ public class SubscriptionTestUtil { private IInterceptorService myInterceptorRegistry; public int getExecutorQueueSize() { - LinkedBlockingChannel channel = mySubscriptionMatcherInterceptor.getProcessingChannelForUnitTest(); + LinkedBlockingChannel channel = (LinkedBlockingChannel) myResourceModifiedSubmitterSvc.getProcessingChannelForUnitTest(); return channel.getQueueSizeForUnitTest(); } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 8193cea4513..c0fffc577ff 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-server-cds-hooks/pom.xml b/hapi-fhir-server-cds-hooks/pom.xml index 3d9b44aeffe..1752eb28f89 100644 --- a/hapi-fhir-server-cds-hooks/pom.xml +++ b/hapi-fhir-server-cds-hooks/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-mdm/pom.xml b/hapi-fhir-server-mdm/pom.xml index 9ac26e25609..5e1f27d2938 100644 --- a/hapi-fhir-server-mdm/pom.xml +++ b/hapi-fhir-server-mdm/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-openapi/pom.xml b/hapi-fhir-server-openapi/pom.xml index a3221fa0bfd..a10cfc8c00e 100644 --- a/hapi-fhir-server-openapi/pom.xml +++ b/hapi-fhir-server-openapi/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index 7c03214ab18..6da4ce5f9ec 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceMessage.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceMessage.java index 6e3b2380f6b..66283ec0682 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceMessage.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceMessage.java @@ -23,8 +23,11 @@ import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.model.api.IModelJson; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.Validate; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -229,4 +232,9 @@ public abstract class BaseResourceMessage implements IResourceMessage, IModelJso return myRestOperationTypeEnum; } } + + @VisibleForTesting + public Map getAttributes() { + return ObjectUtils.defaultIfNull(myAttributes, Collections.emptyMap()); + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceModifiedMessage.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceModifiedMessage.java index 672ff2b2ca9..c98030e643a 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceModifiedMessage.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/messaging/BaseResourceModifiedMessage.java @@ -58,6 +58,9 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im @JsonIgnore protected transient String myPayloadType; + @JsonIgnore + protected String myPayloadVersion; + /** * Constructor */ @@ -101,6 +104,10 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im return myPayloadId; } + public String getPayloadVersion() { + return myPayloadVersion; + } + /** * @since 5.6.0 */ @@ -108,6 +115,7 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im myPayloadId = null; if (thePayloadId != null) { myPayloadId = thePayloadId.toUnqualifiedVersionless().getValue(); + myPayloadVersion = thePayloadId.getVersionIdPart(); } } @@ -138,9 +146,11 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im */ public IIdType getPayloadId(FhirContext theCtx) { IIdType retVal = null; + if (myPayloadId != null) { - retVal = theCtx.getVersion().newIdType().setValue(myPayloadId); + retVal = theCtx.getVersion().newIdType().setValue(myPayloadId).withVersion(myPayloadVersion); } + return retVal; } @@ -172,7 +182,7 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im return ""; } - protected void setNewPayload(FhirContext theCtx, IBaseResource thePayload) { + public void setNewPayload(FhirContext theCtx, IBaseResource thePayload) { /* * References with placeholders would be invalid by the time we get here, and * would be caught before we even get here. This check is basically a last-ditch @@ -246,7 +256,7 @@ public abstract class BaseResourceModifiedMessage extends BaseResourceMessage im @Nullable @Override public String getMessageKeyOrDefault() { - return StringUtils.defaultString(super.getMessageKey(), myPayloadId); + return StringUtils.defaultString(super.getMessageKeyOrNull(), myPayloadId); } public boolean hasPayloadType(FhirContext theFhirContext, @Nonnull String theResourceName) { diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml index c2f66fad694..9d1f0b2a1b3 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-api/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml index 7c1dfdc0417..e72e6e7bbf1 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-caffeine/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../pom.xml @@ -21,7 +21,7 @@ ca.uhn.hapi.fhir hapi-fhir-caching-api - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml index 1ac06d6c405..8619927341b 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-guava/pom.xml @@ -7,7 +7,7 @@ hapi-fhir-serviceloaders ca.uhn.hapi.fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml b/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml index c010bdcc872..b00bc09c1a4 100644 --- a/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml +++ b/hapi-fhir-serviceloaders/hapi-fhir-caching-testing/pom.xml @@ -7,7 +7,7 @@ hapi-fhir ca.uhn.hapi.fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../../pom.xml diff --git a/hapi-fhir-serviceloaders/pom.xml b/hapi-fhir-serviceloaders/pom.xml index c9da6381db2..f4766c1c07e 100644 --- a/hapi-fhir-serviceloaders/pom.xml +++ b/hapi-fhir-serviceloaders/pom.xml @@ -5,7 +5,7 @@ hapi-deployable-pom ca.uhn.hapi.fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml index 6f97b66abb2..d45be5a8738 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml index a676ac2226b..bd44d49aa7f 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-apache/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT hapi-fhir-spring-boot-sample-client-apache diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml index b6cded3d683..06370d1b38e 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-client-okhttp/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml index 17c056e33c0..64eb1571e38 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/hapi-fhir-spring-boot-sample-server-jersey/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot-samples - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml index 49a532b8b60..ef26e60fd55 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-samples/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir-spring-boot - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml index 6dd06516c4c..b5b79a5dc3f 100644 --- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml +++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index 30afce681af..74f674cc765 100644 --- a/hapi-fhir-spring-boot/pom.xml +++ b/hapi-fhir-spring-boot/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-sql-migrate/pom.xml b/hapi-fhir-sql-migrate/pom.xml index 2b5e8db2d2b..be8615db926 100644 --- a/hapi-fhir-sql-migrate/pom.xml +++ b/hapi-fhir-sql-migrate/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2-jobs/pom.xml b/hapi-fhir-storage-batch2-jobs/pom.xml index 61d4b224d52..4499f2b7866 100644 --- a/hapi-fhir-storage-batch2-jobs/pom.xml +++ b/hapi-fhir-storage-batch2-jobs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2-test-utilities/pom.xml b/hapi-fhir-storage-batch2-test-utilities/pom.xml index fa97586fe8f..4a5ca60f520 100644 --- a/hapi-fhir-storage-batch2-test-utilities/pom.xml +++ b/hapi-fhir-storage-batch2-test-utilities/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-batch2/pom.xml b/hapi-fhir-storage-batch2/pom.xml index 7ec6564731d..c2dd3a7939c 100644 --- a/hapi-fhir-storage-batch2/pom.xml +++ b/hapi-fhir-storage-batch2/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-cr/pom.xml b/hapi-fhir-storage-cr/pom.xml index e6218d15cf4..6c4910ee468 100644 --- a/hapi-fhir-storage-cr/pom.xml +++ b/hapi-fhir-storage-cr/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-mdm/pom.xml b/hapi-fhir-storage-mdm/pom.xml index 938ed9845e2..89bebd8d4ba 100644 --- a/hapi-fhir-storage-mdm/pom.xml +++ b/hapi-fhir-storage-mdm/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage-test-utilities/pom.xml b/hapi-fhir-storage-test-utilities/pom.xml index 59180087fe8..09d74f2fc56 100644 --- a/hapi-fhir-storage-test-utilities/pom.xml +++ b/hapi-fhir-storage-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage/pom.xml b/hapi-fhir-storage/pom.xml index 14a2123b723..d1369c1491f 100644 --- a/hapi-fhir-storage/pom.xml +++ b/hapi-fhir-storage/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/matching/IResourceModifiedConsumer.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/matching/IResourceModifiedConsumer.java index 094a5c2110c..72bece8f828 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/matching/IResourceModifiedConsumer.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/match/matcher/matching/IResourceModifiedConsumer.java @@ -20,21 +20,24 @@ package ca.uhn.fhir.jpa.subscription.match.matcher.matching; import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; -import ca.uhn.fhir.rest.api.server.RequestDetails; -import org.hl7.fhir.instance.model.api.IBaseResource; +import ca.uhn.fhir.subscription.api.IResourceModifiedConsumerWithRetries; +import org.springframework.messaging.MessageDeliveryException; +/** + * The implementer of this interface should submit the result of an operation on a resource + * to the subscription processing pipeline. + */ public interface IResourceModifiedConsumer { /** + * Process a message by submitting it to the processing pipeline. The message is assumed to have been successfully + * submitted unless a {@link MessageDeliveryException} is thrown by the underlying support. The exception should be allowed to + * propagate for client handling and potential re-submission through the {@link IResourceModifiedConsumerWithRetries}. + * + * @param theMsg The message to submit + * * This is an internal API - Use with caution! - */ - void submitResourceModified( - IBaseResource theNewResource, - ResourceModifiedMessage.OperationTypeEnum theOperationType, - RequestDetails theRequest); - - /** - * This is an internal API - Use with caution! + * */ void submitResourceModified(ResourceModifiedMessage theMsg); } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/ResourceDeliveryMessage.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/ResourceDeliveryMessage.java index 481538ababb..9cd638d2600 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/ResourceDeliveryMessage.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/subscription/model/ResourceDeliveryMessage.java @@ -158,6 +158,6 @@ public class ResourceDeliveryMessage extends BaseResourceMessage implements IRes @Nullable @Override public String getMessageKeyOrDefault() { - return StringUtils.defaultString(super.getMessageKey(), myPayloadId); + return StringUtils.defaultString(super.getMessageKeyOrNull(), myPayloadId); } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/subscription/api/IResourceModifiedConsumerWithRetries.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/subscription/api/IResourceModifiedConsumerWithRetries.java new file mode 100644 index 00000000000..5a654569118 --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/subscription/api/IResourceModifiedConsumerWithRetries.java @@ -0,0 +1,41 @@ +package ca.uhn.fhir.subscription.api; + +/*- + * #%L + * HAPI FHIR Storage api + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.model.entity.IPersistedResourceModifiedMessage; +import ca.uhn.fhir.jpa.subscription.match.matcher.matching.IResourceModifiedConsumer; + +/** + * The implementer of this interface participates in the retry upon failure mechanism for messages submitted + * to the subscription processing pipeline. + */ +public interface IResourceModifiedConsumerWithRetries { + + /** + * The implementer of this method should submit the ResourceModifiedMessage represented the IPersistedResourceModifiedMessage + * to a broker (see {@link IResourceModifiedConsumer}) and if submission succeeds, delete the IPersistedResourceModifiedMessage. + * + * @param thePersistedResourceModifiedMessage A IPersistedResourceModifiedMessage requiring submission. + * @return Whether the message was successfully submitted to the broker. + */ + boolean submitPersisedResourceModifiedMessage( + IPersistedResourceModifiedMessage thePersistedResourceModifiedMessage); +} diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/subscription/api/IResourceModifiedMessagePersistenceSvc.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/subscription/api/IResourceModifiedMessagePersistenceSvc.java new file mode 100644 index 00000000000..68aad03a48c --- /dev/null +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/subscription/api/IResourceModifiedMessagePersistenceSvc.java @@ -0,0 +1,75 @@ +package ca.uhn.fhir.subscription.api; + +/*- + * #%L + * HAPI FHIR Storage api + * %% + * Copyright (C) 2014 - 2023 Smile CDR, Inc. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.jpa.model.entity.IPersistedResourceModifiedMessage; +import ca.uhn.fhir.jpa.model.entity.IPersistedResourceModifiedMessagePK; +import ca.uhn.fhir.jpa.subscription.model.ResourceModifiedMessage; + +import java.util.List; + +/** + * An implementer of this interface will provide {@link ResourceModifiedMessage} persistence services. + * + * Client of this interface should persist ResourceModifiedMessage as part of the processing of an operation on + * a resource. Upon a successful submission to the subscription pipeline, the persisted message should be deleted. + * When submission fails, the message should be left un-altered for re-submission at a later time (see {@link IResourceModifiedConsumerWithRetries}). + */ +public interface IResourceModifiedMessagePersistenceSvc { + + /** + * Find all persistedResourceModifiedMessage sorted by ascending created dates (oldest to newest). + * + * @return A sorted list of persistedResourceModifiedMessage needing submission. + */ + List findAllOrderedByCreatedTime(); + + /** + * Delete a persistedResourceModifiedMessage by its primary key. + * + * @param thePersistedResourceModifiedMessagePK The primary key of the persistedResourceModifiedMessage to delete. + * @return Whether the persistedResourceModifiedMessage pointed to by theResourceModifiedPK was deleted. + */ + boolean deleteByPK(IPersistedResourceModifiedMessagePK thePersistedResourceModifiedMessagePK); + + /** + * Persist a resourceModifiedMessage and return its resulting persisted representation. + * + * @param theMsg The resourceModifiedMessage to persist. + * @return The persisted representation of theMsg. + */ + IPersistedResourceModifiedMessage persist(ResourceModifiedMessage theMsg); + + /** + * Restore a resourceModifiedMessage to its pre persistence representation. + * + * @param thePersistedResourceModifiedMessage The message needing restoration. + * @return The resourceModifiedMessage in its pre persistence form. + */ + ResourceModifiedMessage inflatePersistedResourceModifiedMessage( + IPersistedResourceModifiedMessage thePersistedResourceModifiedMessage); + + /** + * + * @return the number of persisted resourceModifiedMessage. + */ + long getMessagePersistedCount(); +} diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index bd82f9b3ae3..c79c49fee0a 100644 --- a/hapi-fhir-structures-dstu2.1/pom.xml +++ b/hapi-fhir-structures-dstu2.1/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 3688b898c31..7137a5c425a 100644 --- a/hapi-fhir-structures-dstu2/pom.xml +++ b/hapi-fhir-structures-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index 88b56204c6a..b07c22c3270 100644 --- a/hapi-fhir-structures-dstu3/pom.xml +++ b/hapi-fhir-structures-dstu3/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index 6acd5f37162..0c6b2dddb5a 100644 --- a/hapi-fhir-structures-hl7org-dstu2/pom.xml +++ b/hapi-fhir-structures-hl7org-dstu2/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index d48b73b150b..1e6f1df7015 100644 --- a/hapi-fhir-structures-r4/pom.xml +++ b/hapi-fhir-structures-r4/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4b/pom.xml b/hapi-fhir-structures-r4b/pom.xml index 6a1e6535ef7..92c8903d86f 100644 --- a/hapi-fhir-structures-r4b/pom.xml +++ b/hapi-fhir-structures-r4b/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r5/pom.xml b/hapi-fhir-structures-r5/pom.xml index c0e3b2a14a9..ca8c85d0ea8 100644 --- a/hapi-fhir-structures-r5/pom.xml +++ b/hapi-fhir-structures-r5/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index fb43a3a5932..4a02b8398ac 100644 --- a/hapi-fhir-test-utilities/pom.xml +++ b/hapi-fhir-test-utilities/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 5d07bf1988a..576da06220a 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml index dd5078130b6..d67ba1e918a 100644 --- a/hapi-fhir-validation-resources-dstu2.1/pom.xml +++ b/hapi-fhir-validation-resources-dstu2.1/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu2/pom.xml b/hapi-fhir-validation-resources-dstu2/pom.xml index 0bc9bc9c1db..6b1f99077b0 100644 --- a/hapi-fhir-validation-resources-dstu2/pom.xml +++ b/hapi-fhir-validation-resources-dstu2/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-dstu3/pom.xml b/hapi-fhir-validation-resources-dstu3/pom.xml index 7e2c6dd0900..724125dbb3f 100644 --- a/hapi-fhir-validation-resources-dstu3/pom.xml +++ b/hapi-fhir-validation-resources-dstu3/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r4/pom.xml b/hapi-fhir-validation-resources-r4/pom.xml index 2839ff7c9d8..c4eda275a6a 100644 --- a/hapi-fhir-validation-resources-r4/pom.xml +++ b/hapi-fhir-validation-resources-r4/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r4b/pom.xml b/hapi-fhir-validation-resources-r4b/pom.xml index d177afa22e1..81e65721346 100644 --- a/hapi-fhir-validation-resources-r4b/pom.xml +++ b/hapi-fhir-validation-resources-r4b/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation-resources-r5/pom.xml b/hapi-fhir-validation-resources-r5/pom.xml index 350d22f8560..ef1187e98d9 100644 --- a/hapi-fhir-validation-resources-r5/pom.xml +++ b/hapi-fhir-validation-resources-r5/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index 1918e07a04d..b83747307ec 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index 7d7c6ea61ef..1a291983272 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../pom.xml diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index f3b6f1310c6..c8e857657be 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index b3323c962f9..27e8637bde8 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ ca.uhn.hapi.fhir hapi-fhir pom - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT HAPI-FHIR An open-source implementation of the FHIR specification in Java. diff --git a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml index e8e037360d5..7dac077d23e 100644 --- a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml +++ b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-client/pom.xml b/tests/hapi-fhir-base-test-mindeps-client/pom.xml index 9e23f4dcf2d..85b19b77b41 100644 --- a/tests/hapi-fhir-base-test-mindeps-client/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../../pom.xml diff --git a/tests/hapi-fhir-base-test-mindeps-server/pom.xml b/tests/hapi-fhir-base-test-mindeps-server/pom.xml index 02ff4353082..499e68e1ee9 100644 --- a/tests/hapi-fhir-base-test-mindeps-server/pom.xml +++ b/tests/hapi-fhir-base-test-mindeps-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 6.9.3-SNAPSHOT + 6.9.4-SNAPSHOT ../../pom.xml