From 7a9514b94a5f7af73830e19fc429af9e7aac0b3a Mon Sep 17 00:00:00 2001 From: Nick Goupinets Date: Mon, 29 Mar 2021 20:43:43 -0400 Subject: [PATCH 01/39] Initial fix --- .../fhir/jpa/mdm/svc/MdmEidUpdateService.java | 7 ++++++- .../uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java | 20 ++++++++++--------- .../mdm/rules/matcher/ExtensionMatcher.java | 20 +++++++++++++++++++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmEidUpdateService.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmEidUpdateService.java index c4977891444..94db6849073 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmEidUpdateService.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmEidUpdateService.java @@ -134,6 +134,11 @@ public class MdmEidUpdateService { ourLog.debug(theMessage); } + public void applySurvivorshipRulesAndSaveGoldenResource(IAnyResource theTargetResource, IAnyResource theGoldenResource, MdmTransactionContext theMdmTransactionContext) { + myMdmSurvivorshipService.applySurvivorshipRulesToGoldenResource(theTargetResource, theGoldenResource, theMdmTransactionContext); + myMdmResourceDaoSvc.upsertGoldenResource(theGoldenResource, theMdmTransactionContext.getResourceType()); + } + /** * Data class to hold context surrounding an update operation for an MDM target. */ @@ -162,7 +167,7 @@ public class MdmEidUpdateService { if (theExistingMatchLink.isPresent()) { MdmLink mdmLink = theExistingMatchLink.get(); Long existingGoldenResourcePid = mdmLink.getGoldenResourcePid(); - myExistingGoldenResource = myMdmResourceDaoSvc.readGoldenResourceByPid(new ResourcePersistentId(existingGoldenResourcePid), resourceType); + myExistingGoldenResource = myMdmResourceDaoSvc.readGoldenResourceByPid(new ResourcePersistentId(existingGoldenResourcePid), resourceType); myRemainsMatchedToSameGoldenResource = candidateIsSameAsMdmLinkGoldenResource(mdmLink, theMatchedGoldenResourceCandidate); } else { myRemainsMatchedToSameGoldenResource = false; diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java index 5cac2826956..fdd04914646 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvc.java @@ -46,6 +46,7 @@ import java.util.List; */ @Service public class MdmMatchLinkSvc { + private static final Logger ourLog = Logs.getMdmTroubleshootingLog(); @Autowired @@ -62,7 +63,7 @@ public class MdmMatchLinkSvc { * or create one if one does not exist. Performs matching based on rules defined in mdm-rules.json. * Does nothing if resource is determined to be not managed by MDM. * - * @param theResource the incoming MDM source, which can be any supported MDM type. + * @param theResource the incoming MDM source, which can be any supported MDM type. * @param theMdmTransactionContext * @return an {@link TransactionLogMessages} which contains all informational messages related to MDM processing of this resource. */ @@ -130,20 +131,21 @@ public class MdmMatchLinkSvc { myMdmLinkSvc.updateLink(newGoldenResource, theResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); } - private void handleMdmCreate(IAnyResource theSourceResource, MatchedGoldenResourceCandidate theGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext) { + private void handleMdmCreate(IAnyResource theTargetResource, MatchedGoldenResourceCandidate theGoldenResourceCandidate, MdmTransactionContext theMdmTransactionContext) { log(theMdmTransactionContext, "MDM has narrowed down to one candidate for matching."); - IAnyResource golenResource = myMdmGoldenResourceFindingSvc.getGoldenResourceFromMatchedGoldenResourceCandidate(theGoldenResourceCandidate, theMdmTransactionContext.getResourceType()); + IAnyResource goldenResource = myMdmGoldenResourceFindingSvc.getGoldenResourceFromMatchedGoldenResourceCandidate(theGoldenResourceCandidate, theMdmTransactionContext.getResourceType()); - if (myGoldenResourceHelper.isPotentialDuplicate(golenResource, theSourceResource)) { + if (myGoldenResourceHelper.isPotentialDuplicate(goldenResource, theTargetResource)) { log(theMdmTransactionContext, "Duplicate detected based on the fact that both resources have different external EIDs."); - IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theSourceResource, theMdmTransactionContext); - myMdmLinkSvc.updateLink(newGoldenResource, theSourceResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); - myMdmLinkSvc.updateLink(newGoldenResource, golenResource, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); + IAnyResource newGoldenResource = myGoldenResourceHelper.createGoldenResourceFromMdmSourceResource(theTargetResource, theMdmTransactionContext); + myMdmLinkSvc.updateLink(newGoldenResource, theTargetResource, MdmMatchOutcome.NEW_GOLDEN_RESOURCE_MATCH, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); + myMdmLinkSvc.updateLink(newGoldenResource, goldenResource, MdmMatchOutcome.POSSIBLE_DUPLICATE, MdmLinkSourceEnum.AUTO, theMdmTransactionContext); } else { if (theGoldenResourceCandidate.isMatch()) { - myGoldenResourceHelper.handleExternalEidAddition(golenResource, theSourceResource, theMdmTransactionContext); + myGoldenResourceHelper.handleExternalEidAddition(goldenResource, theTargetResource, theMdmTransactionContext); + myEidUpdateService.applySurvivorshipRulesAndSaveGoldenResource(theTargetResource, goldenResource, theMdmTransactionContext); } - myMdmLinkSvc.updateLink(golenResource, theSourceResource, theGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext); + myMdmLinkSvc.updateLink(goldenResource, theTargetResource, theGoldenResourceCandidate.getMatchResult(), MdmLinkSourceEnum.AUTO, theMdmTransactionContext); } } diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/ExtensionMatcher.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/ExtensionMatcher.java index c71493ca12b..b8faa9763d9 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/ExtensionMatcher.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/ExtensionMatcher.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.mdm.rules.matcher; +/*- + * #%L + * HAPI FHIR - Master Data Management + * %% + * Copyright (C) 2014 - 2021 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.FhirContext; import ca.uhn.fhir.util.ExtensionUtil; import org.hl7.fhir.instance.model.api.IBase; From ab8505dfc4a60e027104162e9eb51c5ec9b5e974 Mon Sep 17 00:00:00 2001 From: Nick Goupinets Date: Wed, 31 Mar 2021 16:07:21 -0400 Subject: [PATCH 02/39] Removed deprecated TerserUtil --- .../jpa/dao/mdm/MdmExpansionCacheSvc.java | 20 ++ .../mdm/svc/GoldenResourceMergerSvcImpl.java | 1 - .../jpa/mdm/svc/MdmSurvivorshipSvcImpl.java | 2 +- ...MdmProviderMergeGoldenResourcesR4Test.java | 2 +- .../java/ca/uhn/fhir/mdm/util/TerserUtil.java | 147 ------------- .../ca/uhn/fhir/mdm/util/TerserUtilTest.java | 197 ------------------ 6 files changed, 22 insertions(+), 347 deletions(-) delete mode 100644 hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/TerserUtil.java delete mode 100644 hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/util/TerserUtilTest.java diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmExpansionCacheSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmExpansionCacheSvc.java index b4315677fec..67d1e528338 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmExpansionCacheSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/mdm/MdmExpansionCacheSvc.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.dao.mdm; +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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 org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/GoldenResourceMergerSvcImpl.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/GoldenResourceMergerSvcImpl.java index fae7c2da3d1..c24d28289d9 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/GoldenResourceMergerSvcImpl.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/GoldenResourceMergerSvcImpl.java @@ -31,7 +31,6 @@ import ca.uhn.fhir.mdm.log.Logs; import ca.uhn.fhir.mdm.model.MdmTransactionContext; import ca.uhn.fhir.mdm.util.GoldenResourceHelper; import ca.uhn.fhir.mdm.util.MdmResourceUtil; -import ca.uhn.fhir.mdm.util.TerserUtil; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import org.hl7.fhir.instance.model.api.IAnyResource; import org.slf4j.Logger; diff --git a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSurvivorshipSvcImpl.java b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSurvivorshipSvcImpl.java index f61fb4bccb0..1dac6473a13 100644 --- a/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSurvivorshipSvcImpl.java +++ b/hapi-fhir-jpaserver-mdm/src/main/java/ca/uhn/fhir/jpa/mdm/svc/MdmSurvivorshipSvcImpl.java @@ -23,7 +23,7 @@ package ca.uhn.fhir.jpa.mdm.svc; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService; import ca.uhn.fhir.mdm.model.MdmTransactionContext; -import ca.uhn.fhir.mdm.util.TerserUtil; +import ca.uhn.fhir.util.TerserUtil; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMergeGoldenResourcesR4Test.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMergeGoldenResourcesR4Test.java index 0bd144b0568..39b5101c93e 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMergeGoldenResourcesR4Test.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/provider/MdmProviderMergeGoldenResourcesR4Test.java @@ -4,9 +4,9 @@ import ca.uhn.fhir.jpa.entity.MdmLink; import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; import ca.uhn.fhir.mdm.util.MdmResourceUtil; -import ca.uhn.fhir.mdm.util.TerserUtil; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; +import ca.uhn.fhir.util.TerserUtil; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.BeforeEach; diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/TerserUtil.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/TerserUtil.java deleted file mode 100644 index d64a55396ea..00000000000 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/util/TerserUtil.java +++ /dev/null @@ -1,147 +0,0 @@ -package ca.uhn.fhir.mdm.util; - -/*- - * #%L - * HAPI FHIR - Master Data Management - * %% - * Copyright (C) 2014 - 2021 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.BaseRuntimeChildDefinition; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.mdm.model.CanonicalEID; -import ca.uhn.fhir.util.FhirTerser; -import org.hl7.fhir.instance.model.api.IBase; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.slf4j.Logger; - -import java.util.Collection; -import java.util.List; -import java.util.function.Predicate; - -import static org.slf4j.LoggerFactory.getLogger; - -@Deprecated -public final class TerserUtil { - private static final Logger ourLog = getLogger(TerserUtil.class); - - public static final Collection IDS_AND_META_EXCLUDES = ca.uhn.fhir.util.TerserUtil.IDS_AND_META_EXCLUDES; - - public static final Predicate EXCLUDE_IDS_AND_META = ca.uhn.fhir.util.TerserUtil.EXCLUDE_IDS_AND_META; - - private TerserUtil() { - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static void cloneEidIntoResource(FhirContext theFhirContext, BaseRuntimeChildDefinition theIdentifierDefinition, IBase theEid, IBase theResourceToCloneEidInto) { - ca.uhn.fhir.util.TerserUtil.cloneEidIntoResource(theFhirContext, theIdentifierDefinition, theEid, theResourceToCloneEidInto); - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static boolean hasValues(FhirContext theFhirContext, IBaseResource theResource, String theFieldName) { - return ca.uhn.fhir.util.TerserUtil.hasValues(theFhirContext, theResource, theFieldName); - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static List getValues(FhirContext theFhirContext, IBaseResource theResource, String theFieldName) { - return ca.uhn.fhir.util.TerserUtil.getValues(theFhirContext, theResource, theFieldName); - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static void cloneCompositeField(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, String field) { - ca.uhn.fhir.util.TerserUtil.cloneCompositeField(theFhirContext, theFrom, theTo, field); - - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static void mergeAllFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo) { - ca.uhn.fhir.util.TerserUtil.mergeAllFields(theFhirContext, theFrom, theTo); - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static void replaceFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, Predicate inclusionStrategy) { - ca.uhn.fhir.util.TerserUtil.replaceFields(theFhirContext, theFrom, theTo, inclusionStrategy); - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static boolean fieldExists(FhirContext theFhirContext, String theFieldName, IBaseResource theInstance) { - return ca.uhn.fhir.util.TerserUtil.fieldExists(theFhirContext, theFieldName, theInstance); - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static void replaceField(FhirContext theFhirContext, String theFieldName, IBaseResource theFrom, IBaseResource theTo) { - ca.uhn.fhir.util.TerserUtil.replaceField(theFhirContext, theFieldName, theFrom, theTo); - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static void replaceField(FhirContext theFhirContext, FhirTerser theTerser, String theFieldName, IBaseResource theFrom, IBaseResource theTo) { - ca.uhn.fhir.util.TerserUtil.replaceField(theFhirContext, theTerser, theFieldName, theFrom, theTo); - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static void mergeFieldsExceptIdAndMeta(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo) { - ca.uhn.fhir.util.TerserUtil.mergeFieldsExceptIdAndMeta(theFhirContext, theFrom, theTo); - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static void mergeFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, Predicate inclusionStrategy) { - ca.uhn.fhir.util.TerserUtil.mergeFields(theFhirContext, theFrom, theTo, inclusionStrategy); - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static void mergeField(FhirContext theFhirContext, String theFieldName, IBaseResource theFrom, IBaseResource theTo) { - ca.uhn.fhir.util.TerserUtil.mergeField(theFhirContext, theFieldName, theFrom, theTo); - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static void mergeField(FhirContext theFhirContext, FhirTerser theTerser, String theFieldName, IBaseResource theFrom, IBaseResource theTo) { - ca.uhn.fhir.util.TerserUtil.mergeField(theFhirContext, theTerser, theFieldName, theFrom, theTo); - } - - /** - * @deprecated Use {@link ca.uhn.fhir.util.TerserUtil} instead - */ - public static T clone(FhirContext theFhirContext, T theInstance) { - return ca.uhn.fhir.util.TerserUtil.clone(theFhirContext, theInstance); - } - -} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/util/TerserUtilTest.java b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/util/TerserUtilTest.java deleted file mode 100644 index a94891fbdd2..00000000000 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/util/TerserUtilTest.java +++ /dev/null @@ -1,197 +0,0 @@ -package ca.uhn.fhir.mdm.util; - -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.mdm.BaseR4Test; -import org.hl7.fhir.r4.model.DateTimeType; -import org.hl7.fhir.r4.model.Extension; -import org.hl7.fhir.r4.model.Identifier; -import org.hl7.fhir.r4.model.Patient; -import org.junit.jupiter.api.Test; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class TerserUtilTest extends BaseR4Test { - - @Test - void testCloneEidIntoResource() { - Identifier identifier = new Identifier().setSystem("http://org.com/sys").setValue("123"); - - Patient p1 = new Patient(); - p1.addIdentifier(identifier); - - Patient p2 = new Patient(); - RuntimeResourceDefinition definition = ourFhirContext.getResourceDefinition(p1); - TerserUtil.cloneEidIntoResource(ourFhirContext, definition.getChildByName("identifier"), identifier, p2); - - assertEquals(1, p2.getIdentifier().size()); - assertEquals(p1.getIdentifier().get(0).getSystem(), p2.getIdentifier().get(0).getSystem()); - assertEquals(p1.getIdentifier().get(0).getValue(), p2.getIdentifier().get(0).getValue()); - } - - @Test - void testFieldExists() { - assertTrue(TerserUtil.fieldExists(ourFhirContext, "identifier", new Patient())); - assertFalse(TerserUtil.fieldExists(ourFhirContext, "randomFieldName", new Patient())); - } - - @Test - void testCloneFields() { - Patient p1 = buildJohny(); - p1.addName().addGiven("Sigizmund"); - p1.setId("Patient/22"); - - Patient p2 = new Patient(); - - TerserUtil.mergeFieldsExceptIdAndMeta(ourFhirContext, p1, p2); - - assertTrue(p2.getIdentifier().isEmpty()); - - assertNull(p2.getId()); - assertEquals(1, p2.getName().size()); - assertEquals(p1.getName().get(0).getNameAsSingleString(), p2.getName().get(0).getNameAsSingleString()); - } - - @Test - void testCloneWithNonPrimitives() { - Patient p1 = new Patient(); - Patient p2 = new Patient(); - - p1.addName().addGiven("Joe"); - p1.getNameFirstRep().addGiven("George"); - assertThat(p1.getName(), hasSize(1)); - assertThat(p1.getName().get(0).getGiven(), hasSize(2)); - - p2.addName().addGiven("Jeff"); - p2.getNameFirstRep().addGiven("George"); - assertThat(p2.getName(), hasSize(1)); - assertThat(p2.getName().get(0).getGiven(), hasSize(2)); - - TerserUtil.mergeAllFields(ourFhirContext, p1, p2); - assertThat(p2.getName(), hasSize(2)); - assertThat(p2.getName().get(0).getGiven(), hasSize(2)); - assertThat(p2.getName().get(1).getGiven(), hasSize(2)); - } - - @Test - void testMergeForAddressWithExtensions() { - Extension ext = new Extension(); - ext.setUrl("http://hapifhir.io/extensions/address#create-timestamp"); - ext.setValue(new DateTimeType("2021-01-02T11:13:15")); - - Patient p1 = new Patient(); - p1.addAddress() - .addLine("10 Main Street") - .setCity("Hamilton") - .setState("ON") - .setPostalCode("Z0Z0Z0") - .setCountry("Canada") - .addExtension(ext); - - Patient p2 = new Patient(); - p2.addAddress().addLine("10 Lenin Street").setCity("Severodvinsk").setCountry("Russia"); - - TerserUtil.mergeField(ourFhirContext,"address", p1, p2); - - assertEquals(2, p2.getAddress().size()); - assertEquals("[10 Lenin Street]", p2.getAddress().get(0).getLine().toString()); - assertEquals("[10 Main Street]", p2.getAddress().get(1).getLine().toString()); - assertTrue(p2.getAddress().get(1).hasExtension()); - - p1 = new Patient(); - p1.addAddress().addLine("10 Main Street").addExtension(ext); - p2 = new Patient(); - p2.addAddress().addLine("10 Main Street").addExtension(new Extension("demo", new DateTimeType("2021-01-02"))); - - TerserUtil.mergeField(ourFhirContext,"address", p1, p2); - assertEquals(2, p2.getAddress().size()); - assertTrue(p2.getAddress().get(0).hasExtension()); - assertTrue(p2.getAddress().get(1).hasExtension()); - - } - - @Test - void testReplaceForAddressWithExtensions() { - Extension ext = new Extension(); - ext.setUrl("http://hapifhir.io/extensions/address#create-timestamp"); - ext.setValue(new DateTimeType("2021-01-02T11:13:15")); - - Patient p1 = new Patient(); - p1.addAddress() - .addLine("10 Main Street") - .setCity("Hamilton") - .setState("ON") - .setPostalCode("Z0Z0Z0") - .setCountry("Canada") - .addExtension(ext); - - Patient p2 = new Patient(); - p2.addAddress().addLine("10 Lenin Street").setCity("Severodvinsk").setCountry("Russia"); - - TerserUtil.replaceField(ourFhirContext,"address", p1, p2); - - assertEquals(1, p2.getAddress().size()); - assertEquals("[10 Main Street]", p2.getAddress().get(0).getLine().toString()); - assertTrue(p2.getAddress().get(0).hasExtension()); - } - - @Test - void testMergeForSimilarAddresses() { - Extension ext = new Extension(); - ext.setUrl("http://hapifhir.io/extensions/address#create-timestamp"); - ext.setValue(new DateTimeType("2021-01-02T11:13:15")); - - Patient p1 = new Patient(); - p1.addAddress() - .addLine("10 Main Street") - .setCity("Hamilton") - .setState("ON") - .setPostalCode("Z0Z0Z0") - .setCountry("Canada") - .addExtension(ext); - - Patient p2 = new Patient(); - p2.addAddress() - .addLine("10 Main Street") - .setCity("Hamilton") - .setState("ON") - .setPostalCode("Z0Z0Z1") - .setCountry("Canada") - .addExtension(ext); - - TerserUtil.mergeField(ourFhirContext,"address", p1, p2); - - assertEquals(2, p2.getAddress().size()); - assertEquals("[10 Main Street]", p2.getAddress().get(0).getLine().toString()); - assertEquals("[10 Main Street]", p2.getAddress().get(1).getLine().toString()); - assertTrue(p2.getAddress().get(1).hasExtension()); - } - - - @Test - void testCloneWithDuplicateNonPrimitives() { - Patient p1 = new Patient(); - Patient p2 = new Patient(); - - p1.addName().addGiven("Jim"); - p1.getNameFirstRep().addGiven("George"); - - assertThat(p1.getName(), hasSize(1)); - assertThat(p1.getName().get(0).getGiven(), hasSize(2)); - - p2.addName().addGiven("Jim"); - p2.getNameFirstRep().addGiven("George"); - - assertThat(p2.getName(), hasSize(1)); - assertThat(p2.getName().get(0).getGiven(), hasSize(2)); - - TerserUtil.mergeAllFields(ourFhirContext, p1, p2); - - assertThat(p2.getName(), hasSize(1)); - assertThat(p2.getName().get(0).getGiven(), hasSize(2)); - } -} From 0cd14b971ac94185ddf31e407ca9d085f1506e27 Mon Sep 17 00:00:00 2001 From: Nick Goupinets Date: Thu, 1 Apr 2021 10:59:50 -0400 Subject: [PATCH 03/39] Added changelog --- .../5_4_0/2515-mdm-survivorship-rules-application.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2515-mdm-survivorship-rules-application.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2515-mdm-survivorship-rules-application.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2515-mdm-survivorship-rules-application.yaml new file mode 100644 index 00000000000..aa93351b4b4 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2515-mdm-survivorship-rules-application.yaml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 2515 +title: "Addresses MDM survivorship rules application on matching a single resource" From 9629aceff06ddea00bb1d5f25063b6176f60481b Mon Sep 17 00:00:00 2001 From: Nick Goupinets Date: Thu, 1 Apr 2021 15:52:52 -0400 Subject: [PATCH 04/39] Removed obsolete test and added more control to replace fields in TU --- .../java/ca/uhn/fhir/util/TerserUtil.java | 41 +++++++++++++++---- .../fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java | 22 ---------- .../java/ca/uhn/fhir/util/TerserUtilTest.java | 17 ++++++++ 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtil.java index fc7e8fd0ced..126b2dfca3b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtil.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; import ca.uhn.fhir.context.BaseRuntimeElementDefinition; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; +import org.apache.commons.lang3.tuple.Triple; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseResource; import org.slf4j.Logger; @@ -58,6 +59,17 @@ public final class TerserUtil { } }; + public static final Predicate> EXCLUDE_IDS_META_AND_EMPTY = new Predicate>() { + @Override + public boolean test(Triple theTriple) { + if (!EXCLUDE_IDS_AND_META.test(theTriple.getLeft().getElementName())) { + return false; + } + + return theTriple.getLeft().getAccessor().getValues(theTriple.getRight()).isEmpty(); + } + }; + public static final Predicate INCLUDE_ALL = new Predicate() { @Override public boolean test(String s) { @@ -235,20 +247,35 @@ public final class TerserUtil { } /** - * Replaces all fields that test positive by the given inclusion strategy. theTo will contain a copy of the + * Replaces all fields that have matching field names by the given inclusion strategy. theTo will contain a copy of the * values from theFrom instance. * - * @param theFhirContext Context holding resource definition - * @param theFrom The resource to merge the fields from - * @param theTo The resource to merge the fields into - * @param inclusionStrategy Inclusion strategy that checks if a given field should be replaced by checking {@link Predicate#test(Object)} + * @param theFhirContext Context holding resource definition + * @param theFrom The resource to merge the fields from + * @param theTo The resource to merge the fields into + * @param theFieldNameInclusion Inclusion strategy that checks if a given field should be replaced */ - public static void replaceFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, Predicate inclusionStrategy) { + public static void replaceFields(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, Predicate theFieldNameInclusion) { + Predicate> predicate + = (t) -> theFieldNameInclusion.test(t.getLeft().getElementName()); + replaceFieldsByPredicate(theFhirContext, theFrom, theTo, predicate); + } + + /** + * Replaces empty fields on theTo resource that test positive by the given predicate. theTo will contain a copy of the + * values from theFrom for which predicate tests positive. + * + * @param theFhirContext Context holding resource definition + * @param theFrom The resource to merge the fields from + * @param theTo The resource to merge the fields into + * @param thePredicate Predicate that checks if a given field should be replaced + */ + public static void replaceFieldsByPredicate(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, Predicate> thePredicate) { FhirTerser terser = theFhirContext.newTerser(); RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom); for (BaseRuntimeChildDefinition childDefinition : definition.getChildrenAndExtension()) { - if (!inclusionStrategy.test(childDefinition.getElementName())) { + if (!thePredicate.test(Triple.of(childDefinition, theFrom, theTo))) { continue; } diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java index 06eb9ebe9d4..b3236ccaa39 100644 --- a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcTest.java @@ -476,28 +476,6 @@ public class MdmMatchLinkSvcTest extends BaseMdmR4Test { assertThat(nameFirstRep.getGivenAsSingleString(), is(equalToIgnoringCase("paul"))); } - @Test - public void testPatientCreateDoesNotOverwriteGoldenResourceAttributesThatAreInvolvedInLinking() { - Patient paul = buildPaulPatient(); - paul.setGender(Enumerations.AdministrativeGender.MALE); - paul = createPatientAndUpdateLinks(paul); - - Patient sourcePatientFromTarget = (Patient) getGoldenResourceFromTargetResource(paul); - - assertThat(sourcePatientFromTarget.getGender(), is(equalTo(Enumerations.AdministrativeGender.MALE))); - - Patient paul2 = buildPaulPatient(); - paul2.setGender(Enumerations.AdministrativeGender.FEMALE); - paul2 = createPatientAndUpdateLinks(paul2); - - assertThat(paul2, is(sameGoldenResourceAs(paul))); - - //Newly matched patients aren't allowed to overwrite GoldenResource Attributes unless they are empty, - // so gender should still be set to male. - Patient paul2GoldenResource = (Patient) getGoldenResourceFromTargetResource(paul2); - assertThat(paul2GoldenResource.getGender(), is(equalTo(Enumerations.AdministrativeGender.MALE))); - } - @Test //Test Case #1 public void testPatientUpdatesOverwriteGoldenResourceData() { diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/TerserUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/TerserUtilTest.java index d4af78f0162..a94efbfb845 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/TerserUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/TerserUtilTest.java @@ -77,6 +77,7 @@ class TerserUtilTest { assertEquals(check.getValue(), p.getBirthDate()); } + @Test void testFieldExists() { assertTrue(TerserUtil.fieldExists(ourFhirContext, "identifier", TerserUtil.newResource(ourFhirContext, "Patient"))); @@ -294,6 +295,22 @@ class TerserUtilTest { assertEquals("Doe", p2.getName().get(0).getFamily()); } + @Test + public void testReplaceFieldsByPredicate() { + Patient p1 = new Patient(); + p1.addName().setFamily("Doe"); + p1.setGender(Enumerations.AdministrativeGender.MALE); + + Patient p2 = new Patient(); + p2.addName().setFamily("Smith"); + + TerserUtil.replaceFieldsByPredicate(ourFhirContext, p1, p2, TerserUtil.EXCLUDE_IDS_META_AND_EMPTY); + + assertEquals(1, p2.getName().size()); + assertEquals("Smith", p2.getName().get(0).getFamily()); + assertEquals(Enumerations.AdministrativeGender.MALE, p2.getGender()); + } + @Test public void testClearFields() { Patient p1 = new Patient(); From d05b585426a57ecbc23b8936e3d0871af5003462 Mon Sep 17 00:00:00 2001 From: Nick Goupinets Date: Thu, 1 Apr 2021 19:38:53 -0400 Subject: [PATCH 05/39] Added a test for survivorship svc invocation --- .../svc/MdmMatchLinkSvcSurvivorshipTest.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcSurvivorshipTest.java diff --git a/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcSurvivorshipTest.java b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcSurvivorshipTest.java new file mode 100644 index 00000000000..2a1d563d085 --- /dev/null +++ b/hapi-fhir-jpaserver-mdm/src/test/java/ca/uhn/fhir/jpa/mdm/svc/MdmMatchLinkSvcSurvivorshipTest.java @@ -0,0 +1,70 @@ +package ca.uhn.fhir.jpa.mdm.svc; + +import ca.uhn.fhir.jpa.entity.MdmLink; +import ca.uhn.fhir.jpa.mdm.BaseMdmR4Test; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.mdm.api.IMdmLinkSvc; +import ca.uhn.fhir.mdm.api.IMdmSurvivorshipService; +import ca.uhn.fhir.mdm.api.MdmConstants; +import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; +import ca.uhn.fhir.mdm.api.MdmMatchOutcome; +import ca.uhn.fhir.mdm.model.MdmTransactionContext; +import ca.uhn.fhir.mdm.util.GoldenResourceHelper; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.param.TokenParam; +import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; + +import javax.annotation.Nullable; + +import static ca.uhn.fhir.mdm.api.MdmMatchResultEnum.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.times; +import static org.slf4j.LoggerFactory.getLogger; + +public class MdmMatchLinkSvcSurvivorshipTest extends BaseMdmR4Test { + + private static final Logger ourLog = getLogger(MdmMatchLinkSvcSurvivorshipTest.class); + + @Autowired + IMdmLinkSvc myMdmLinkSvc; + + @SpyBean + IMdmSurvivorshipService myMdmSurvivorshipService; + + @Autowired + private GoldenResourceHelper myGoldenResourceHelper; + + @Captor + ArgumentCaptor myPatientCaptor; + @Captor + ArgumentCaptor myContext; + + @Test + public void testSurvivorshipIsCalledOnMatchingToTheSameGoldenResource() { + // no candidates + createPatientAndUpdateLinks(buildJanePatient()); + verifySurvivorshipCalled(1); + + // single candidate + createPatientAndUpdateLinks(buildJanePatient()); + verifySurvivorshipCalled(2); + + // multiple candidates matching to the same golden record + createPatientAndUpdateLinks(buildJanePatient()); + verifySurvivorshipCalled(3); + } + + private void verifySurvivorshipCalled(int theNumberOfTimes) { + Mockito.verify(myMdmSurvivorshipService, times(theNumberOfTimes)).applySurvivorshipRulesToGoldenResource(myPatientCaptor.capture(), myPatientCaptor.capture(), myContext.capture()); + } +} From 5a3f2e3edfcb6c0184d3a25ad36fab9951d898ef Mon Sep 17 00:00:00 2001 From: ianmarshall Date: Tue, 13 Apr 2021 22:22:06 -0400 Subject: [PATCH 06/39] Change package loader to not generate snapshot for logical StructureDefinition resources. --- .../jpa/packages/PackageInstallerSvcImpl.java | 10 ++++- .../ca/uhn/fhir/jpa/packages/NpmR4Test.java | 36 ++++++++++++++++++ .../test-logical-structuredefinition.tgz | Bin 0 -> 4711 bytes 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/test/resources/packages/test-logical-structuredefinition.tgz diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java index b8cfc721732..beb787dc32e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java @@ -404,9 +404,15 @@ public class PackageInstallerSvcImpl implements IPackageInstallerSvc { } private boolean isStructureDefinitionWithoutSnapshot(IBaseResource r) { + boolean retVal = false; FhirTerser terser = myFhirContext.newTerser(); - return r.getClass().getSimpleName().equals("StructureDefinition") && - terser.getSingleValueOrNull(r, "snapshot") == null; + if (r.getClass().getSimpleName().equals("StructureDefinition")) { + Optional kind = terser.getSinglePrimitiveValue(r, "kind"); + if (kind.isPresent() && !(kind.get().equals("logical"))) { + retVal = terser.getSingleValueOrNull(r, "snapshot") == null; + } + } + return retVal; } private IBaseResource generateSnapshot(IBaseResource sd) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java index 05940239896..ec088316919 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/packages/NpmR4Test.java @@ -69,7 +69,9 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -734,6 +736,40 @@ public class NpmR4Test extends BaseJpaR4Test { }); } + @Test + public void testInstallPkgContainingLogicalStructureDefinition() throws Exception { + myDaoConfig.setAllowExternalReferences(true); + + byte[] bytes = loadClasspathBytes("/packages/test-logical-structuredefinition.tgz"); + myFakeNpmServlet.myResponses.put("/test-logical-structuredefinition/1.0.0", bytes); + + PackageInstallationSpec spec = new PackageInstallationSpec().setName("test-logical-structuredefinition").setVersion("1.0.0").setInstallMode(PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL); + PackageInstallOutcomeJson outcome = myPackageInstallerSvc.install(spec); + assertEquals(2, outcome.getResourcesInstalled().get("StructureDefinition")); + + // Be sure no further communication with the server + JettyUtil.closeServer(myServer); + + // Search for the installed resource + runInTransaction(() -> { + // Confirm that Laborbefund (a logical StructureDefinition) was created without a snapshot. + SearchParameterMap map = SearchParameterMap.newSynchronous(); + map.add(StructureDefinition.SP_URL, new UriParam("https://www.medizininformatik-initiative.de/fhir/core/modul-labor/StructureDefinition/LogicalModel/Laborbefund")); + IBundleProvider result = myStructureDefinitionDao.search(map); + assertEquals(1, result.sizeOrThrowNpe()); + List resources = result.getResources(0,1); + assertFalse(((StructureDefinition)resources.get(0)).hasSnapshot()); + + // Confirm that DiagnosticLab (a resource StructureDefinition with differential but no snapshot) was created with a generated snapshot. + map = SearchParameterMap.newSynchronous(); + map.add(StructureDefinition.SP_URL, new UriParam("https://www.medizininformatik-initiative.de/fhir/core/modul-labor/StructureDefinition/DiagnosticReportLab")); + result = myStructureDefinitionDao.search(map); + assertEquals(1, result.sizeOrThrowNpe()); + resources = result.getResources(0,1); + assertTrue(((StructureDefinition)resources.get(0)).hasSnapshot()); + + }); + } static class FakeNpmServlet extends HttpServlet { diff --git a/hapi-fhir-jpaserver-base/src/test/resources/packages/test-logical-structuredefinition.tgz b/hapi-fhir-jpaserver-base/src/test/resources/packages/test-logical-structuredefinition.tgz new file mode 100644 index 0000000000000000000000000000000000000000..035b48ff77167ca3f2cd97e67a54a9ce7c2f8bd1 GIT binary patch literal 4711 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 literal 0 HcmV?d00001 From 0cb95ad55632db7dbc034e394a683cd945b4f16a Mon Sep 17 00:00:00 2001 From: ianmarshall Date: Tue, 13 Apr 2021 22:30:22 -0400 Subject: [PATCH 07/39] Change package loader to not generate snapshot for logical StructureDefinition resources. --- ...not-create-snapshot-for-logical-structuredefinition.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2535-do-not-create-snapshot-for-logical-structuredefinition.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2535-do-not-create-snapshot-for-logical-structuredefinition.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2535-do-not-create-snapshot-for-logical-structuredefinition.yaml new file mode 100644 index 00000000000..6f3e83a7a3b --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2535-do-not-create-snapshot-for-logical-structuredefinition.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 2535 +title: "An issue with package installer involving logical StructureDefinition resources was fixed. Package registry will no + longer attempt to generate a snapshot for logical StructureDefinition resources if one is not already provided in the + resource definition." From b45ddcc3da1f03cfb07d38bcdbac8b62fe7a0f71 Mon Sep 17 00:00:00 2001 From: Kevin Dougan SmileCDR <72025369+KevinDougan-SmileCDR@users.noreply.github.com> Date: Wed, 14 Apr 2021 08:45:49 -0400 Subject: [PATCH 08/39] 2543 - Fix issue where versionned references are not being returned properly. (#2544) * 2543 - Fix issue where versionned references are not being returned properly. * 2543 - Added changelog entries for this fix plus a previous fix for 2533. --- ...-not-being-returned-in-search-queries.yaml | 6 ++ ...ences-are-not-being-returned-properly.yaml | 8 ++ ...irResourceDaoR4VersionedReferenceTest.java | 78 +++++++++++++++++++ .../fhir/jpa/model/entity/ResourceLink.java | 4 + 4 files changed, 96 insertions(+) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2533-fix-issue-with-reference-resources-not-being-returned-in-search-queries.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2543-fix-issue-where-versionned-references-are-not-being-returned-properly.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2533-fix-issue-with-reference-resources-not-being-returned-in-search-queries.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2533-fix-issue-with-reference-resources-not-being-returned-in-search-queries.yaml new file mode 100644 index 00000000000..fae2c3de9a6 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2533-fix-issue-with-reference-resources-not-being-returned-in-search-queries.yaml @@ -0,0 +1,6 @@ +--- +type: fix +issue: 2533 +title: "When issuing a request for a specific Resource and also specifying an _include param, +the referenced resource is not returned when there is only 1 version of the referenced resource available. +When there are more than 1 versions available, the referenced resource is returned in the response bundle." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2543-fix-issue-where-versionned-references-are-not-being-returned-properly.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2543-fix-issue-where-versionned-references-are-not-being-returned-properly.yaml new file mode 100644 index 00000000000..aa180bb0423 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2543-fix-issue-where-versionned-references-are-not-being-returned-properly.yaml @@ -0,0 +1,8 @@ +--- +type: fix +issue: 2543 +title: "When issuing a request for a specific Resource and also specifying an _include param, +the proper historical referenced resource is not returned when there are more than 1 versions of the +referenced resource available, after the reference has been changed from the original version 1 to some other version. +When there are more than 1 versions available, and the referring resource had previously referred to version 1 +but now refers to version 4, the resource returned in the response bundle is for version 1." diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java index bda53b10805..ec689d1a673 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4VersionedReferenceTest.java @@ -444,6 +444,84 @@ public class FhirResourceDaoR4VersionedReferenceTest extends BaseJpaR4Test { assertEquals(conditionId.withVersion("1").getValue(), resources.get(1).getIdElement().getValue()); } + @Test + public void testSearchAndIncludeVersionedReference_WhenMultipleVersionsExist() { + HashSet refPaths = new HashSet(); + refPaths.add("Task.basedOn"); + myFhirCtx.getParserOptions().setDontStripVersionsFromReferencesAtPaths(refPaths); + myModelConfig.setRespectVersionsForSearchIncludes(true); + myFhirCtx.getParserOptions().setStripVersionsFromReferences(false); + + // Create a Condition + Condition condition = new Condition(); + IIdType conditionId = myConditionDao.create(condition).getId().toUnqualified(); + + // Now, update the Condition 3 times to generate a 4th version of it + condition.setRecordedDate(new Date(System.currentTimeMillis())); + conditionId = myConditionDao.update(condition).getId(); + condition.setRecordedDate(new Date(System.currentTimeMillis() + 1000000)); + conditionId = myConditionDao.update(condition).getId(); + condition.setRecordedDate(new Date(System.currentTimeMillis() + 2000000)); + conditionId = myConditionDao.update(condition).getId(); + + // Create a Task which is basedOn that Condition + Task task = new Task(); + task.setBasedOn(Arrays.asList(new Reference(conditionId))); + IIdType taskId = myTaskDao.create(task).getId().toUnqualified(); + + // Search for the Task using an _include=Task.basedOn and make sure we get the Condition resource in the Response + IBundleProvider outcome = myTaskDao.search(SearchParameterMap.newSynchronous().addInclude(Task.INCLUDE_BASED_ON)); + assertEquals(2, outcome.size()); + List resources = outcome.getResources(0, 2); + assertEquals(2, resources.size(), resources.stream().map(t->t.getIdElement().toUnqualified().getValue()).collect(Collectors.joining(", "))); + assertEquals(taskId.getValue(), resources.get(0).getIdElement().getValue()); + assertEquals(conditionId.getValue(), ((Task)resources.get(0)).getBasedOn().get(0).getReference()); + assertEquals(conditionId.withVersion("4").getValue(), resources.get(1).getIdElement().getValue()); + } + + @Test + public void testSearchAndIncludeVersionedReference_WhenPreviouslyReferencedVersionOne() { + HashSet refPaths = new HashSet(); + refPaths.add("Task.basedOn"); + myFhirCtx.getParserOptions().setDontStripVersionsFromReferencesAtPaths(refPaths); + myModelConfig.setRespectVersionsForSearchIncludes(true); + myFhirCtx.getParserOptions().setStripVersionsFromReferences(false); + + // Create a Condition + Condition condition = new Condition(); + IIdType conditionId = myConditionDao.create(condition).getId().toUnqualified(); + ourLog.info("conditionId: \n{}", conditionId); + + // Create a Task which is basedOn that Condition + Task task = new Task(); + task.setBasedOn(Arrays.asList(new Reference(conditionId))); + IIdType taskId = myTaskDao.create(task).getId().toUnqualified(); + + // Now, update the Condition 3 times to generate a 4th version of it + condition.setRecordedDate(new Date(System.currentTimeMillis())); + conditionId = myConditionDao.update(condition).getId(); + ourLog.info("UPDATED conditionId: \n{}", conditionId); + condition.setRecordedDate(new Date(System.currentTimeMillis() + 1000000)); + conditionId = myConditionDao.update(condition).getId(); + ourLog.info("UPDATED conditionId: \n{}", conditionId); + condition.setRecordedDate(new Date(System.currentTimeMillis() + 2000000)); + conditionId = myConditionDao.update(condition).getId(); + ourLog.info("UPDATED conditionId: \n{}", conditionId); + + // Now, update the Task to refer to the latest version 4 of the Condition + task.setBasedOn(Arrays.asList(new Reference(conditionId))); + taskId = myTaskDao.update(task).getId(); + ourLog.info("UPDATED taskId: \n{}", taskId); + + // Search for the Task using an _include=Task.basedOn and make sure we get the Condition resource in the Response + IBundleProvider outcome = myTaskDao.search(SearchParameterMap.newSynchronous().addInclude(Task.INCLUDE_BASED_ON)); + assertEquals(2, outcome.size()); + List resources = outcome.getResources(0, 2); + assertEquals(2, resources.size(), resources.stream().map(t->t.getIdElement().toUnqualified().getValue()).collect(Collectors.joining(", "))); + assertEquals(taskId.getValue(), resources.get(0).getIdElement().getValue()); + assertEquals(conditionId.getValue(), ((Task)resources.get(0)).getBasedOn().get(0).getReference()); + assertEquals(conditionId.withVersion("4").getValue(), resources.get(1).getIdElement().getValue()); + } @Test public void testSearchAndIncludeUnersionedReference_Asynchronous() { diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceLink.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceLink.java index 98cf2561bfd..581c0de16b0 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceLink.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/ResourceLink.java @@ -138,6 +138,7 @@ public class ResourceLink extends BaseResourceIndex { b.append(mySourceResource, obj.mySourceResource); b.append(myTargetResourceUrl, obj.myTargetResourceUrl); b.append(myTargetResourceType, obj.myTargetResourceType); + b.append(myTargetResourceVersion, obj.myTargetResourceVersion); b.append(getTargetResourceId(), obj.getTargetResourceId()); return b.isEquals(); } @@ -150,6 +151,7 @@ public class ResourceLink extends BaseResourceIndex { myTargetResourceId = source.getTargetResourceId(); myTargetResourcePid = source.getTargetResourcePid(); myTargetResourceType = source.getTargetResourceType(); + myTargetResourceVersion = source.getTargetResourceVersion(); myTargetResourceUrl = source.getTargetResourceUrl(); } @@ -244,6 +246,7 @@ public class ResourceLink extends BaseResourceIndex { b.append(mySourcePath); b.append(mySourceResource); b.append(myTargetResourceUrl); + b.append(myTargetResourceVersion); b.append(getTargetResourceType()); b.append(getTargetResourceId()); return b.toHashCode(); @@ -257,6 +260,7 @@ public class ResourceLink extends BaseResourceIndex { b.append(", src=").append(mySourceResourcePid); b.append(", target=").append(myTargetResourcePid); b.append(", targetType=").append(myTargetResourceType); + b.append(", targetVersion=").append(myTargetResourceVersion); b.append(", targetUrl=").append(myTargetResourceUrl); b.append("]"); From 550602b2f173efe8a8e33cb42322aacef7889658 Mon Sep 17 00:00:00 2001 From: Ken Stevens Date: Wed, 14 Apr 2021 13:15:30 -0400 Subject: [PATCH 09/39] added numeric matcher (#2547) * added numeric matcher * changelog * performance optimization * fix test --- .../fhir/context/phonetic/NumericEncoder.java | 18 +++++++++ .../context/phonetic/PhoneticEncoderEnum.java | 3 +- .../context/phonetic/PhoneticEncoderTest.java | 12 ++++-- .../5_4_0/2547-mdm-add-numeric-matcher.yaml | 5 +++ .../fhir/docs/server_jpa_mdm/mdm_rules.md | 12 +++++- ...esourceDaoDstu3PhoneticSearchNoFtTest.java | 32 +++++++++++++--- .../mdm/rules/matcher/MdmMatcherEnum.java | 3 +- .../mdm/rules/matcher/NumericMatcher.java | 16 ++++++++ .../rules/matcher/StringMatcherR4Test.java | 37 ++++++++++++------- 9 files changed, 110 insertions(+), 28 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/NumericEncoder.java create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2547-mdm-add-numeric-matcher.yaml create mode 100644 hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NumericMatcher.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/NumericEncoder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/NumericEncoder.java new file mode 100644 index 00000000000..1619748d470 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/NumericEncoder.java @@ -0,0 +1,18 @@ +package ca.uhn.fhir.context.phonetic; + +import com.google.common.base.CharMatcher; + +// Useful for numerical identifiers like phone numbers, address parts etc. +// This should not be used where decimals are important. A new "quantity encoder" should be added to handle cases like that. +public class NumericEncoder implements IPhoneticEncoder { + @Override + public String name() { + return "NUMERIC"; + } + + @Override + public String encode(String theString) { + // Remove everything but the numbers + return CharMatcher.inRange('0', '9').retainFrom(theString); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderEnum.java index 28549a71629..605a8ae24ca 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderEnum.java @@ -39,7 +39,8 @@ public enum PhoneticEncoderEnum { METAPHONE(new ApacheEncoder("METAPHONE", new Metaphone())), NYSIIS(new ApacheEncoder("NYSIIS", new Nysiis())), REFINED_SOUNDEX(new ApacheEncoder("REFINED_SOUNDEX", new RefinedSoundex())), - SOUNDEX(new ApacheEncoder("SOUNDEX", new Soundex())); + SOUNDEX(new ApacheEncoder("SOUNDEX", new Soundex())), + NUMERIC(new NumericEncoder()); private final IPhoneticEncoder myPhoneticEncoder; diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderTest.java index bca150978cb..e43327eb818 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/context/phonetic/PhoneticEncoderTest.java @@ -1,14 +1,14 @@ package ca.uhn.fhir.context.phonetic; -import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertEquals; class PhoneticEncoderTest { private static final Logger ourLog = LoggerFactory.getLogger(PhoneticEncoderTest.class); @@ -23,7 +23,11 @@ class PhoneticEncoderTest { public void testEncodeAddress(PhoneticEncoderEnum thePhoneticEncoderEnum) { String encoded = thePhoneticEncoderEnum.getPhoneticEncoder().encode(ADDRESS_LINE); ourLog.info("{}: {}", thePhoneticEncoderEnum.name(), encoded); - assertThat(encoded, startsWith(NUMBER + " ")); - assertThat(encoded, endsWith(" " + SUITE)); + if (thePhoneticEncoderEnum == PhoneticEncoderEnum.NUMERIC) { + assertEquals(NUMBER + SUITE, encoded); + } else { + assertThat(encoded, startsWith(NUMBER + " ")); + assertThat(encoded, endsWith(" " + SUITE)); + } } } diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2547-mdm-add-numeric-matcher.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2547-mdm-add-numeric-matcher.yaml new file mode 100644 index 00000000000..24aace91fdf --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2547-mdm-add-numeric-matcher.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 2547 +title: "Added new NUMERIC mdm matcher for matching phone numbers. Also added NUMERIC phonetic encoder to support +adding NUMERIC encoded search parameter (e.g. if searching for matching phone numbers is required by mdm candidate searching)." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_rules.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_rules.md index a4fea7f82d0..3075b67543d 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_rules.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_jpa_mdm/mdm_rules.md @@ -292,10 +292,10 @@ The following algorithms are currently supported: Gail = Gael, Gail != Gale, Thomas != Tom - CAVERPHONE1 + CAVERPHONE2 matcher - Apache Caverphone1 + Apache Caverphone2 Gail = Gael, Gail = Gale, Thomas != Tom @@ -379,6 +379,14 @@ The following algorithms are currently supported: 2019-12,Month = 2019-12-19,Day + + NUMERIC + matcher + + Remove all non-numeric characters from the string before comparing. + + 4169671111 = (416) 967-1111 + NAME_ANY_ORDER matcher diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3PhoneticSearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3PhoneticSearchNoFtTest.java index bda399dbd76..75b74c87aa8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3PhoneticSearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3PhoneticSearchNoFtTest.java @@ -1,12 +1,13 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.context.phonetic.ApacheEncoder; +import ca.uhn.fhir.context.phonetic.NumericEncoder; import ca.uhn.fhir.context.phonetic.PhoneticEncoderEnum; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; -import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.util.HapiExtensions; import org.apache.commons.codec.language.Soundex; import org.hl7.fhir.dstu3.model.Enumerations; @@ -35,10 +36,14 @@ public class FhirResourceDaoDstu3PhoneticSearchNoFtTest extends BaseJpaDstu3Test public static final String GAIL = "Gail"; public static final String NAME_SOUNDEX_SP = "nameSoundex"; public static final String ADDRESS_LINE_SOUNDEX_SP = "addressLineSoundex"; + public static final String PHONE_NUMBER_SP = "phoneNumber"; private static final String BOB = "BOB"; private static final String ADDRESS = "123 Nohili St"; private static final String ADDRESS_CLOSE = "123 Nohily St"; private static final String ADDRESS_FAR = "123 College St"; + private static final String PHONE = "4169671111"; + private static final String PHONE_CLOSE = "(416) 967-1111"; + private static final String PHONE_FAR = "416 421 0421"; @Autowired ISearchParamRegistry mySearchParamRegistry; @@ -49,8 +54,9 @@ public class FhirResourceDaoDstu3PhoneticSearchNoFtTest extends BaseJpaDstu3Test myDaoConfig.setReuseCachedSearchResultsForMillis(null); myDaoConfig.setFetchSizeDefaultMaximum(new DaoConfig().getFetchSizeDefaultMaximum()); - createSoundexSearchParameter(NAME_SOUNDEX_SP, PhoneticEncoderEnum.SOUNDEX, "Patient.name"); - createSoundexSearchParameter(ADDRESS_LINE_SOUNDEX_SP, PhoneticEncoderEnum.SOUNDEX, "Patient.address.line"); + createPhoneticSearchParameter(NAME_SOUNDEX_SP, PhoneticEncoderEnum.SOUNDEX, "Patient.name"); + createPhoneticSearchParameter(ADDRESS_LINE_SOUNDEX_SP, PhoneticEncoderEnum.SOUNDEX, "Patient.address.line"); + createPhoneticSearchParameter(PHONE_NUMBER_SP, PhoneticEncoderEnum.NUMERIC, "Patient.telecom"); mySearchParamRegistry.forceRefresh(); mySearchParamRegistry.setPhoneticEncoder(new ApacheEncoder(PhoneticEncoderEnum.SOUNDEX.name(), new Soundex())); } @@ -70,6 +76,15 @@ public class FhirResourceDaoDstu3PhoneticSearchNoFtTest extends BaseJpaDstu3Test ourLog.info("Encoded address: {}", soundex.encode(ADDRESS)); } + @Test + public void testNumeric() { + NumericEncoder numeric = new NumericEncoder(); + assertEquals(PHONE, numeric.encode(PHONE_CLOSE)); + assertEquals(PHONE, numeric.encode(PHONE)); + assertEquals(numeric.encode(PHONE), numeric.encode(PHONE_CLOSE)); + assertNotEquals(numeric.encode(PHONE), numeric.encode(PHONE_FAR)); + } + @Test public void phoneticMatch() { Patient patient; @@ -77,15 +92,16 @@ public class FhirResourceDaoDstu3PhoneticSearchNoFtTest extends BaseJpaDstu3Test patient = new Patient(); patient.addName().addGiven(GALE); patient.addAddress().addLine(ADDRESS); + patient.addTelecom().setValue(PHONE); ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient)); IIdType pId = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); List stringParams = myResourceIndexedSearchParamStringDao.findAll(); - assertThat(stringParams, hasSize(6)); + assertThat(stringParams, hasSize(7)); List stringParamNames = stringParams.stream().map(ResourceIndexedSearchParamString::getParamName).collect(Collectors.toList()); - assertThat(stringParamNames, containsInAnyOrder(Patient.SP_NAME, Patient.SP_GIVEN, Patient.SP_PHONETIC, NAME_SOUNDEX_SP, Patient.SP_ADDRESS, ADDRESS_LINE_SOUNDEX_SP)); + assertThat(stringParamNames, containsInAnyOrder(Patient.SP_NAME, Patient.SP_GIVEN, Patient.SP_PHONETIC, NAME_SOUNDEX_SP, Patient.SP_ADDRESS, ADDRESS_LINE_SOUNDEX_SP, PHONE_NUMBER_SP)); assertSearchMatch(pId, Patient.SP_PHONETIC, GALE); assertSearchMatch(pId, Patient.SP_PHONETIC, GAIL); @@ -98,6 +114,10 @@ public class FhirResourceDaoDstu3PhoneticSearchNoFtTest extends BaseJpaDstu3Test assertSearchMatch(pId, ADDRESS_LINE_SOUNDEX_SP, ADDRESS); assertSearchMatch(pId, ADDRESS_LINE_SOUNDEX_SP, ADDRESS_CLOSE); assertNoMatch(ADDRESS_LINE_SOUNDEX_SP, ADDRESS_FAR); + + assertSearchMatch(pId, PHONE_NUMBER_SP, PHONE); + assertSearchMatch(pId, PHONE_NUMBER_SP, PHONE_CLOSE); + assertNoMatch(PHONE_NUMBER_SP, PHONE_FAR); } private void assertSearchMatch(IIdType thePId1, String theSp, String theValue) { @@ -114,7 +134,7 @@ public class FhirResourceDaoDstu3PhoneticSearchNoFtTest extends BaseJpaDstu3Test assertThat(toUnqualifiedVersionlessIdValues(myPatientDao.search(map)), hasSize(0)); } - private void createSoundexSearchParameter(String theCode, PhoneticEncoderEnum theEncoder, String theFhirPath) { + private void createPhoneticSearchParameter(String theCode, PhoneticEncoderEnum theEncoder, String theFhirPath) { SearchParameter searchParameter = new SearchParameter(); searchParameter.addBase("Patient"); searchParameter.setCode(theCode); diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmMatcherEnum.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmMatcherEnum.java index 458387d14d3..f29dad1827c 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmMatcherEnum.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/MdmMatcherEnum.java @@ -51,7 +51,8 @@ public enum MdmMatcherEnum { IDENTIFIER(new IdentifierMatcher()), EMPTY_FIELD(new EmptyFieldMatcher()), - EXTENSION_ANY_ORDER(new ExtensionMatcher()); + EXTENSION_ANY_ORDER(new ExtensionMatcher()), + NUMERIC(new HapiStringMatcher(new NumericMatcher())); private final IMdmFieldMatcher myMdmFieldMatcher; diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NumericMatcher.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NumericMatcher.java new file mode 100644 index 00000000000..82bce7d59c0 --- /dev/null +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NumericMatcher.java @@ -0,0 +1,16 @@ +package ca.uhn.fhir.mdm.rules.matcher; + +import ca.uhn.fhir.context.phonetic.NumericEncoder; + +// Useful for numerical identifiers like phone numbers, address parts etc. +// This should not be used where decimals are important. A new "quantity matcher" should be added to handle cases like that. +public class NumericMatcher implements IMdmStringMatcher { + private final NumericEncoder encoder = new NumericEncoder(); + + @Override + public boolean matches(String theLeftString, String theRightString) { + String left = encoder.encode(theLeftString); + String right = encoder.encode(theRightString); + return left.equals(right); + } +} diff --git a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringMatcherR4Test.java b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringMatcherR4Test.java index 42508046adc..73aafb7aebe 100644 --- a/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringMatcherR4Test.java +++ b/hapi-fhir-server-mdm/src/test/java/ca/uhn/fhir/mdm/rules/matcher/StringMatcherR4Test.java @@ -14,24 +14,33 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class StringMatcherR4Test extends BaseMatcherR4Test { private static final Logger ourLog = LoggerFactory.getLogger(StringMatcherR4Test.class); - public static final String LEFT = "namadega"; - public static final String RIGHT = "namaedga"; + public static final String LEFT_NAME = "namadega"; + public static final String RIGHT_NAME = "namaedga"; @Test public void testNamadega() { - assertTrue(match(MdmMatcherEnum.COLOGNE, LEFT, RIGHT)); - assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, LEFT, RIGHT)); - assertTrue(match(MdmMatcherEnum.MATCH_RATING_APPROACH, LEFT, RIGHT)); - assertTrue(match(MdmMatcherEnum.METAPHONE, LEFT, RIGHT)); - assertTrue(match(MdmMatcherEnum.SOUNDEX, LEFT, RIGHT)); - assertTrue(match(MdmMatcherEnum.METAPHONE, LEFT, RIGHT)); + String left = LEFT_NAME; + String right = RIGHT_NAME; + assertTrue(match(MdmMatcherEnum.COLOGNE, left, right)); + assertTrue(match(MdmMatcherEnum.DOUBLE_METAPHONE, left, right)); + assertTrue(match(MdmMatcherEnum.MATCH_RATING_APPROACH, left, right)); + assertTrue(match(MdmMatcherEnum.METAPHONE, left, right)); + assertTrue(match(MdmMatcherEnum.SOUNDEX, left, right)); + assertTrue(match(MdmMatcherEnum.METAPHONE, left, right)); - assertFalse(match(MdmMatcherEnum.CAVERPHONE1, LEFT, RIGHT)); - assertFalse(match(MdmMatcherEnum.CAVERPHONE2, LEFT, RIGHT)); - assertFalse(match(MdmMatcherEnum.NYSIIS, LEFT, RIGHT)); - assertFalse(match(MdmMatcherEnum.REFINED_SOUNDEX, LEFT, RIGHT)); - assertFalse(match(MdmMatcherEnum.STRING, LEFT, RIGHT)); - assertFalse(match(MdmMatcherEnum.SUBSTRING, LEFT, RIGHT)); + assertFalse(match(MdmMatcherEnum.CAVERPHONE1, left, right)); + assertFalse(match(MdmMatcherEnum.CAVERPHONE2, left, right)); + assertFalse(match(MdmMatcherEnum.NYSIIS, left, right)); + assertFalse(match(MdmMatcherEnum.REFINED_SOUNDEX, left, right)); + assertFalse(match(MdmMatcherEnum.STRING, left, right)); + assertFalse(match(MdmMatcherEnum.SUBSTRING, left, right)); + } + + @Test + public void testNumeric() { + assertTrue(match(MdmMatcherEnum.NUMERIC, "4169671111", "(416) 967-1111")); + assertFalse(match(MdmMatcherEnum.NUMERIC, "5169671111", "(416) 967-1111")); + assertFalse(match(MdmMatcherEnum.NUMERIC, "4169671111", "(416) 967-1111x123")); } @Test From 2ebc57b8a27ed0a13d6eead50d0b267c6d9fe2ee Mon Sep 17 00:00:00 2001 From: Kevin Dougan SmileCDR <72025369+KevinDougan-SmileCDR@users.noreply.github.com> Date: Wed, 14 Apr 2021 13:33:07 -0400 Subject: [PATCH 10/39] 2548 - Add backport changelog Tag to backported fixes for #2533 and #2543. (#2549) --- ...reference-resources-not-being-returned-in-search-queries.yaml | 1 + ...re-versionned-references-are-not-being-returned-properly.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2533-fix-issue-with-reference-resources-not-being-returned-in-search-queries.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2533-fix-issue-with-reference-resources-not-being-returned-in-search-queries.yaml index fae2c3de9a6..5172ef0dbf5 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2533-fix-issue-with-reference-resources-not-being-returned-in-search-queries.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2533-fix-issue-with-reference-resources-not-being-returned-in-search-queries.yaml @@ -4,3 +4,4 @@ issue: 2533 title: "When issuing a request for a specific Resource and also specifying an _include param, the referenced resource is not returned when there is only 1 version of the referenced resource available. When there are more than 1 versions available, the referenced resource is returned in the response bundle." +backport: 5.3.2 diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2543-fix-issue-where-versionned-references-are-not-being-returned-properly.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2543-fix-issue-where-versionned-references-are-not-being-returned-properly.yaml index aa180bb0423..e9f7e2cfa7d 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2543-fix-issue-where-versionned-references-are-not-being-returned-properly.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2543-fix-issue-where-versionned-references-are-not-being-returned-properly.yaml @@ -6,3 +6,4 @@ the proper historical referenced resource is not returned when there are more th referenced resource available, after the reference has been changed from the original version 1 to some other version. When there are more than 1 versions available, and the referring resource had previously referred to version 1 but now refers to version 4, the resource returned in the response bundle is for version 1." +backport: 5.3.2 From ca2088f3adf7c985af7b14aeff402efc75ba8aab Mon Sep 17 00:00:00 2001 From: James Agnew Date: Wed, 14 Apr 2021 17:41:32 -0400 Subject: [PATCH 11/39] Add framework for Bulk Import (#2538) * Start work on bul;k import * Work on bulk import * Have batch working * Working * Working * More work * More work on bulk export * Address fixmes * License header updates * Test fixes * License header updates * Test fix * Test fix * Version bumps * Work on config * Test cleanup * One more version bump * Version bump * CLeanup * A few additions * Test fixes * Test fix * Test fix * Migration fix * Test fix * Test fix --- .editorconfig | 1 + hapi-deployable-pom/pom.xml | 2 +- hapi-fhir-android/pom.xml | 2 +- hapi-fhir-base/pom.xml | 2 +- .../java/ca/uhn/fhir/util/BundleBuilder.java | 3 +- .../ca/uhn/fhir/i18n/hapi-messages.properties | 4 +- hapi-fhir-bom/pom.xml | 4 +- hapi-fhir-cli/hapi-fhir-cli-api/pom.xml | 2 +- hapi-fhir-cli/hapi-fhir-cli-app/pom.xml | 2 +- hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml | 2 +- hapi-fhir-cli/pom.xml | 2 +- hapi-fhir-client-okhttp/pom.xml | 2 +- hapi-fhir-client/pom.xml | 2 +- hapi-fhir-converter/pom.xml | 2 +- hapi-fhir-dist/pom.xml | 2 +- hapi-fhir-docs/pom.xml | 8 +- .../hapi/fhir/changelog/5_4_0/changes.yaml | 25 + hapi-fhir-jacoco/pom.xml | 2 +- hapi-fhir-jaxrsserver-base/pom.xml | 2 +- hapi-fhir-jaxrsserver-example/pom.xml | 2 +- hapi-fhir-jpaserver-api/pom.xml | 2 +- .../uhn/fhir/jpa/api/dao/IFhirSystemDao.java | 12 + hapi-fhir-jpaserver-base/pom.xml | 2 +- .../uhn/fhir/jpa/batch/BatchJobsConfig.java | 9 +- .../GoldenResourceAnnotatingProcessor.java | 2 +- .../api/BulkDataExportOptions.java | 2 +- .../{ => export}/api/IBulkDataExportSvc.java | 10 +- .../{ => export}/job/BaseBulkItemReader.java | 5 +- .../BulkExportCreateEntityStepListener.java | 12 +- ...portGenerateResourceFilesStepListener.java | 8 +- .../{ => export}/job/BulkExportJobCloser.java | 10 +- .../{ => export}/job/BulkExportJobConfig.java | 31 +- .../job/BulkExportJobParameterValidator.java | 3 +- .../job/BulkExportJobParametersBuilder.java | 4 +- .../bulk/{ => export}/job/BulkItemReader.java | 5 +- .../job/CreateBulkExportEntityTasklet.java | 8 +- .../GroupBulkExportJobParametersBuilder.java | 2 +- .../{ => export}/job/GroupBulkItemReader.java | 5 +- .../job/GroupIdPresentValidator.java | 4 +- .../job/PatientBulkItemReader.java | 4 +- .../job/ResourceToFileWriter.java | 4 +- .../job/ResourceTypePartitioner.java | 4 +- .../model/BulkExportJobStatusEnum.java} | 11 +- .../model/BulkExportResponseJson.java | 2 +- .../provider/BulkDataExportProvider.java | 8 +- .../svc/BulkDataExportSvcImpl.java | 31 +- .../svc/BulkExportCollectionFileDaoSvc.java | 2 +- .../{ => export}/svc/BulkExportDaoSvc.java | 10 +- .../bulk/imprt/api/IBulkDataImportSvc.java | 93 +++ .../ActivateBulkImportEntityStepListener.java | 51 ++ .../bulk/imprt/job/BulkImportFileReader.java | 76 ++ .../bulk/imprt/job/BulkImportFileWriter.java | 74 ++ .../bulk/imprt/job/BulkImportJobCloser.java | 57 ++ .../bulk/imprt/job/BulkImportJobConfig.java | 169 +++++ .../job/BulkImportJobParameterValidator.java | 70 ++ .../bulk/imprt/job/BulkImportPartitioner.java | 72 ++ ...BulkImportProcessStepCompletionPolicy.java | 41 ++ .../imprt/job/BulkImportStepListener.java | 63 ++ .../job/CreateBulkImportEntityTasklet.java | 45 ++ .../imprt/model/BulkImportJobFileJson.java | 51 ++ .../bulk/imprt/model/BulkImportJobJson.java | 72 ++ .../imprt/model/BulkImportJobStatusEnum.java | 34 + .../model/JobFileRowProcessingModeEnum.java | 34 + .../imprt/model/ParsedBulkImportRecord.java | 46 ++ .../bulk/imprt/svc/BulkDataImportSvcImpl.java | 280 ++++++++ .../ca/uhn/fhir/jpa/config/BaseConfig.java | 50 +- .../uhn/fhir/jpa/config/BaseDstu2Config.java | 9 + .../jpa/config/dstu3/BaseDstu3Config.java | 5 - .../uhn/fhir/jpa/config/r4/BaseR4Config.java | 5 - .../uhn/fhir/jpa/config/r5/BaseR5Config.java | 5 - .../fhir/jpa/dao/BaseHapiFhirSystemDao.java | 32 +- .../jpa/dao/BaseTransactionProcessor.java | 32 +- .../uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java | 661 ------------------ ...ansactionProcessorVersionAdapterDstu2.java | 171 +++++ .../fhir/jpa/dao/data/IBulkExportJobDao.java | 6 +- .../fhir/jpa/dao/data/IBulkImportJobDao.java | 40 ++ .../jpa/dao/data/IBulkImportJobFileDao.java | 43 ++ .../jpa/dao/dstu3/FhirSystemDaoDstu3.java | 16 - .../dao/expunge/ExpungeEverythingService.java | 4 + .../uhn/fhir/jpa/dao/r4/FhirSystemDaoR4.java | 28 - .../uhn/fhir/jpa/dao/r5/FhirSystemDaoR5.java | 22 - .../fhir/jpa/entity/BulkExportJobEntity.java | 12 +- .../fhir/jpa/entity/BulkImportJobEntity.java | 157 +++++ .../jpa/entity/BulkImportJobFileEntity.java | 104 +++ .../uhn/fhir/jpa/bulk/BaseBatchJobR4Test.java | 58 ++ .../jpa/bulk/BulkDataExportProviderTest.java | 16 +- .../jpa/bulk/BulkDataExportSvcImplR4Test.java | 130 ++-- .../bulk/imprt/svc/BulkDataImportR4Test.java | 155 ++++ .../imprt/svc/BulkDataImportSvcImplTest.java | 145 ++++ .../java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java | 2 +- .../jpa/dao/TransactionProcessorTest.java | 2 +- .../fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java | 2 +- .../jpa/dao/dstu2/FhirSystemDaoDstu2Test.java | 8 +- .../fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java | 2 +- .../ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java | 12 +- ...esourceDaoR4SearchWithElasticSearchIT.java | 2 +- ...urceDaoR4SearchWithLuceneDisabledTest.java | 2 +- ...sourceDaoR4TerminologyElasticsearchIT.java | 2 +- .../fhir/jpa/dao/r4/FhirSystemDaoR4Test.java | 61 +- .../ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java | 2 +- .../ValueSetExpansionR4ElasticsearchIT.java | 2 +- hapi-fhir-jpaserver-batch/pom.xml | 2 +- .../ca/uhn/fhir/jpa/batch/BatchConstants.java | 32 + .../config/NonPersistedBatchConfigurer.java | 3 +- hapi-fhir-jpaserver-cql/pom.xml | 6 +- hapi-fhir-jpaserver-mdm/pom.xml | 6 +- hapi-fhir-jpaserver-migrate/pom.xml | 2 +- .../tasks/HapiFhirJpaMigrationTasks.java | 27 + hapi-fhir-jpaserver-model/pom.xml | 2 +- hapi-fhir-jpaserver-searchparam/pom.xml | 2 +- hapi-fhir-jpaserver-subscription/pom.xml | 2 +- hapi-fhir-jpaserver-test-utilities/pom.xml | 2 +- hapi-fhir-jpaserver-uhnfhirtest/pom.xml | 4 +- .../ca/uhn/fhirtest/TestRestfulServer.java | 2 +- hapi-fhir-server-mdm/pom.xml | 2 +- hapi-fhir-server/pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../hapi-fhir-spring-boot-samples/pom.xml | 2 +- .../hapi-fhir-spring-boot-starter/pom.xml | 2 +- hapi-fhir-spring-boot/pom.xml | 2 +- hapi-fhir-structures-dstu2.1/pom.xml | 2 +- hapi-fhir-structures-dstu2/pom.xml | 2 +- ...eThymeleafNarrativeGeneratorDstu2Test.java | 86 ++- ...mThymeleafNarrativeGeneratorDstu2Test.java | 17 +- ...tThymeleafNarrativeGeneratorDstu2Test.java | 28 +- hapi-fhir-structures-dstu3/pom.xml | 2 +- ...tThymeleafNarrativeGeneratorDstu3Test.java | 32 +- hapi-fhir-structures-hl7org-dstu2/pom.xml | 2 +- hapi-fhir-structures-r4/pom.xml | 2 +- ...stomThymeleafNarrativeGeneratorR4Test.java | 135 ++-- ...aultThymeleafNarrativeGeneratorR4Test.java | 8 +- hapi-fhir-structures-r5/pom.xml | 2 +- hapi-fhir-test-utilities/pom.xml | 2 +- .../fhir/test/utilities/ITestDataBuilder.java | 2 +- hapi-fhir-testpage-overlay/pom.xml | 2 +- .../pom.xml | 2 +- hapi-fhir-validation-resources-dstu2/pom.xml | 2 +- hapi-fhir-validation-resources-dstu3/pom.xml | 2 +- hapi-fhir-validation-resources-r4/pom.xml | 2 +- hapi-fhir-validation-resources-r5/pom.xml | 2 +- hapi-fhir-validation/pom.xml | 2 +- hapi-tinder-plugin/pom.xml | 16 +- hapi-tinder-test/pom.xml | 2 +- pom.xml | 80 +-- restful-server-example/pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- 151 files changed, 2891 insertions(+), 1279 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/api/BulkDataExportOptions.java (98%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/api/IBulkDataExportSvc.java (91%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/BaseBulkItemReader.java (98%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/BulkExportCreateEntityStepListener.java (78%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/BulkExportGenerateResourceFilesStepListener.java (88%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/BulkExportJobCloser.java (83%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/BulkExportJobConfig.java (88%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/BulkExportJobParameterValidator.java (98%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/BulkExportJobParametersBuilder.java (95%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/BulkItemReader.java (94%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/CreateBulkExportEntityTasklet.java (93%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/GroupBulkExportJobParametersBuilder.java (96%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/GroupBulkItemReader.java (98%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/GroupIdPresentValidator.java (93%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/PatientBulkItemReader.java (97%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/ResourceToFileWriter.java (97%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/job/ResourceTypePartitioner.java (96%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{model/BulkJobStatusEnum.java => export/model/BulkExportJobStatusEnum.java} (77%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/model/BulkExportResponseJson.java (98%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/provider/BulkDataExportProvider.java (98%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/svc/BulkDataExportSvcImpl.java (95%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/svc/BulkExportCollectionFileDaoSvc.java (96%) rename hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/{ => export}/svc/BulkExportDaoSvc.java (90%) create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/api/IBulkDataImportSvc.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/ActivateBulkImportEntityStepListener.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportFileReader.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportFileWriter.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportJobCloser.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportJobConfig.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportJobParameterValidator.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportPartitioner.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportProcessStepCompletionPolicy.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportStepListener.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/CreateBulkImportEntityTasklet.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/BulkImportJobFileJson.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/BulkImportJobJson.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/BulkImportJobStatusEnum.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/JobFileRowProcessingModeEnum.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/ParsedBulkImportRecord.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImpl.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessorVersionAdapterDstu2.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBulkImportJobDao.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBulkImportJobFileDao.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BulkImportJobEntity.java create mode 100644 hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BulkImportJobFileEntity.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BaseBatchJobR4Test.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportR4Test.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImplTest.java create mode 100644 hapi-fhir-jpaserver-batch/src/main/java/ca/uhn/fhir/jpa/batch/BatchConstants.java diff --git a/.editorconfig b/.editorconfig index f19de7e2a01..479bb985c23 100644 --- a/.editorconfig +++ b/.editorconfig @@ -31,6 +31,7 @@ charset = utf-8 indent_style = tab tab_width = 3 indent_size = 3 +continuation_indent_size=3 ij_java_align_consecutive_assignments = false ij_java_align_consecutive_variable_declarations = false ij_java_align_group_field_declarations = false diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index 0bee4719670..2925cddee83 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index 9eb400b46c0..60f23bd7bcc 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index ac1fd654c29..00ce73745e2 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java index ca79ffb009b..7e3c568663e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java @@ -156,7 +156,8 @@ public class BundleBuilder { // Bundle.entry.request.url IPrimitiveType url = (IPrimitiveType) myContext.getElementDefinition("uri").newInstance(); - url.setValueAsString(theResource.getIdElement().toUnqualifiedVersionless().getValue()); + String resourceType = myContext.getResourceType(theResource); + url.setValueAsString(theResource.getIdElement().toUnqualifiedVersionless().withResourceType(resourceType).getValue()); myEntryRequestUrlChild.getMutator().setValue(request, url); // Bundle.entry.request.url diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties index 26c705f6f74..e1e02ff0e12 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/i18n/hapi-messages.properties @@ -68,8 +68,8 @@ ca.uhn.fhir.validation.ValidationResult.noIssuesDetected=No issues detected duri # JPA Messages -ca.uhn.fhir.jpa.bulk.svc.BulkDataExportSvcImpl.onlyBinarySelected=Binary resources may not be exported with bulk export -ca.uhn.fhir.jpa.bulk.svc.BulkDataExportSvcImpl.unknownResourceType=Unknown or unsupported resource type: {0} +ca.uhn.fhir.jpa.bulk.export.svc.BulkDataExportSvcImpl.onlyBinarySelected=Binary resources may not be exported with bulk export +ca.uhn.fhir.jpa.bulk.export.svc.BulkDataExportSvcImpl.unknownResourceType=Unknown or unsupported resource type: {0} ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceVersionConstraintFailure=The operation has failed with a version constraint failure. This generally means that two clients/threads were trying to update the same resource at the same time, and this request was chosen as the failing request. ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.resourceIndexedCompositeStringUniqueConstraintFailure=The operation has failed with a unique index constraint failure. This probably means that the operation was trying to create/update a resource that would have resulted in a duplicate value for a unique index. ca.uhn.fhir.jpa.config.HapiFhirHibernateJpaDialect.forcedIdConstraintFailure=The operation has failed with a client-assigned ID constraint failure. This typically means that multiple client threads are trying to create a new resource with the same client-assigned ID at the same time, and this thread was chosen to be rejected. diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index 6d6a435eddf..d3c726265b4 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -3,14 +3,14 @@ 4.0.0 ca.uhn.hapi.fhir hapi-fhir-bom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT pom HAPI FHIR BOM ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/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 b9a5e93b0f6..2816ae6159e 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-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 d7306fba4a9..7d9dc89fe6c 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml index 4d1fd59e786..1055a8ed4ea 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../../hapi-deployable-pom diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index ac83c296578..8784078d56b 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index 78ec60b5d68..cef760eeff8 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index 0783c0ea778..e7ac14275c6 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index ce5eca39f05..2ec2f22795c 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index 837b2992aee..24b559ea001 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index 58d067d2797..40519292d48 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -78,13 +78,13 @@ ca.uhn.hapi.fhir hapi-fhir-structures-dstu2 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT compile ca.uhn.hapi.fhir hapi-fhir-jpaserver-subscription - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT compile @@ -101,7 +101,7 @@ ca.uhn.hapi.fhir hapi-fhir-testpage-overlay - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT classes diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml new file mode 100644 index 00000000000..c973340bfd7 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml @@ -0,0 +1,25 @@ +--- +- item: + type: "add" + title: "The version of a few dependencies have been bumped to the latest versions +(dependent HAPI modules listed in brackets): +
    +
  • Commons-Lang3 (Core): 3.9 -> 3.12.0
  • +
  • Commons-Text (Core): 1.7 -> 1.9
  • +
  • Commons-Codec (Core): 1.14 -> 1.15
  • +
  • Commons-IO (Core): 2.6 -> 2.8.0
  • +
  • Guava (Core): 30.1-jre -> 30.1.1-jre
  • +
  • Jackson (Core): 2.12.1 -> 2.12.3
  • +
  • Woodstox (Core): 6.2.3 -> 6.2.5
  • +
  • Gson (JPA): 2.8.5 -> 2.8.6
  • +
  • Caffeine (JPA): 2.7.0 -> 3.0.1
  • +
  • Hibernate (JPA): 5.4.26.Final -> 5.4.30.Final
  • +
  • Hibernate Search (JPA): 6.0.0.Final -> 6.0.2.Final
  • +
  • Spring (JPA): 5.3.3 -> 5.3.6
  • +
  • Spring Batch (JPA): 4.2.3.RELEASE -> 4.3.2
  • +
  • Spring Data (JPA): 2.4.2 -> 2.4.7
  • +
  • Commons DBCP2 (JPA): 2.7.0 -> 2.8.0
  • +
  • Thymeleaf (Testpage Overlay): 3.0.11.RELEASE -> 3.0.12.RELEASE
  • +
  • JAnsi (CLI): 2.1.1 -> 2.3.2
  • +
+" diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index bddf4b9e81b..43dd8b85ed7 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index 4d6434eab74..073ecc3dbf0 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-example/pom.xml b/hapi-fhir-jaxrsserver-example/pom.xml index 627009d073a..cce18f1d9c1 100644 --- a/hapi-fhir-jaxrsserver-example/pom.xml +++ b/hapi-fhir-jaxrsserver-example/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-jpaserver-api/pom.xml b/hapi-fhir-jpaserver-api/pom.xml index a1b87034a93..ac534aaccba 100644 --- a/hapi-fhir-jpaserver-api/pom.xml +++ b/hapi-fhir-jpaserver-api/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirSystemDao.java b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirSystemDao.java index 2d810788d76..00b2ffb8027 100644 --- a/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirSystemDao.java +++ b/hapi-fhir-jpaserver-api/src/main/java/ca/uhn/fhir/jpa/api/dao/IFhirSystemDao.java @@ -67,6 +67,18 @@ public interface IFhirSystemDao extends IDao { */ IBaseBundle processMessage(RequestDetails theRequestDetails, IBaseBundle theMessage); + /** + * Executes a FHIR transaction using a new database transaction. This method must + * not be called from within a DB transaction. + */ T transaction(RequestDetails theRequestDetails, T theResources); + /** + * Executes a FHIR transaction nested inside the current database transaction. + * This form of the transaction processor can handle write operations only (no reads) + */ + default T transactionNested(RequestDetails theRequestDetails, T theResources) { + throw new UnsupportedOperationException(); + } + } diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 7b0116aad18..eedfef43910 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch/BatchJobsConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch/BatchJobsConfig.java index 178ee7358d5..fc4c03c7b34 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch/BatchJobsConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch/BatchJobsConfig.java @@ -20,17 +20,20 @@ package ca.uhn.fhir.jpa.batch; * #L% */ -import ca.uhn.fhir.jpa.bulk.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.bulk.export.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.bulk.imprt.job.BulkImportJobConfig; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @Configuration //When you define a new batch job, add it here. @Import({ - CommonBatchJobConfig.class, - BulkExportJobConfig.class + CommonBatchJobConfig.class, + BulkExportJobConfig.class, + BulkImportJobConfig.class }) public class BatchJobsConfig { + public static final String BULK_IMPORT_JOB_NAME = "bulkImportJob"; public static final String BULK_EXPORT_JOB_NAME = "bulkExportJob"; public static final String GROUP_BULK_EXPORT_JOB_NAME = "groupBulkExportJob"; public static final String PATIENT_BULK_EXPORT_JOB_NAME = "patientBulkExportJob"; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch/processors/GoldenResourceAnnotatingProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch/processors/GoldenResourceAnnotatingProcessor.java index 4566aa83af3..46c418a1e74 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch/processors/GoldenResourceAnnotatingProcessor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/batch/processors/GoldenResourceAnnotatingProcessor.java @@ -24,7 +24,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.fhirpath.IFhirPath; import ca.uhn.fhir.jpa.batch.log.Logs; -import ca.uhn.fhir.jpa.bulk.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.bulk.export.job.BulkExportJobConfig; import ca.uhn.fhir.jpa.dao.mdm.MdmExpansionCacheSvc; import ca.uhn.fhir.util.ExtensionUtil; import ca.uhn.fhir.util.HapiExtensions; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/api/BulkDataExportOptions.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/api/BulkDataExportOptions.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/api/BulkDataExportOptions.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/api/BulkDataExportOptions.java index c63a0df0546..4f50d6fed97 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/api/BulkDataExportOptions.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/api/BulkDataExportOptions.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.api; +package ca.uhn.fhir.jpa.bulk.export.api; /*- * #%L diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/api/IBulkDataExportSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/api/IBulkDataExportSvc.java similarity index 91% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/api/IBulkDataExportSvc.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/api/IBulkDataExportSvc.java index af39667b19b..bbd1d1a6628 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/api/IBulkDataExportSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/api/IBulkDataExportSvc.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.api; +package ca.uhn.fhir.jpa.bulk.export.api; /*- * #%L @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.bulk.api; * #L% */ -import ca.uhn.fhir.jpa.bulk.model.BulkJobStatusEnum; +import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum; import org.hl7.fhir.instance.model.api.IIdType; import javax.transaction.Transactional; @@ -50,7 +50,7 @@ public interface IBulkDataExportSvc { class JobInfo { private String myJobId; - private BulkJobStatusEnum myStatus; + private BulkExportJobStatusEnum myStatus; private List myFiles; private String myRequest; private Date myStatusTime; @@ -90,11 +90,11 @@ public interface IBulkDataExportSvc { } - public BulkJobStatusEnum getStatus() { + public BulkExportJobStatusEnum getStatus() { return myStatus; } - public JobInfo setStatus(BulkJobStatusEnum theStatus) { + public JobInfo setStatus(BulkExportJobStatusEnum theStatus) { myStatus = theStatus; return this; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BaseBulkItemReader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BaseBulkItemReader.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BaseBulkItemReader.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BaseBulkItemReader.java index 9ab5e56a75d..7f934cfb248 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BaseBulkItemReader.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BaseBulkItemReader.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -30,7 +30,6 @@ import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; import ca.uhn.fhir.jpa.dao.data.IBulkExportJobDao; import ca.uhn.fhir.jpa.entity.BulkExportJobEntity; -import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.searchparam.MatchUrlService; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; @@ -102,7 +101,7 @@ public abstract class BaseBulkItemReader implements ItemReader getResourcePidIterator(); + protected abstract Iterator getResourcePidIterator(); protected List createSearchParameterMapsForResourceType() { BulkExportJobEntity jobEntity = getJobEntity(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportCreateEntityStepListener.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportCreateEntityStepListener.java similarity index 78% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportCreateEntityStepListener.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportCreateEntityStepListener.java index 96b25dc4073..8da195c344c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportCreateEntityStepListener.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportCreateEntityStepListener.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -20,16 +20,12 @@ package ca.uhn.fhir.jpa.bulk.job; * #L% */ -import ca.uhn.fhir.jpa.bulk.model.BulkJobStatusEnum; -import ca.uhn.fhir.jpa.bulk.svc.BulkExportDaoSvc; -import org.springframework.batch.core.BatchStatus; +import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum; +import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportDaoSvc; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepExecutionListener; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; - -import static org.apache.commons.lang3.StringUtils.isNotBlank; /** * Will run before and after a job to set the status to whatever is appropriate. @@ -43,7 +39,7 @@ public class BulkExportCreateEntityStepListener implements StepExecutionListener public void beforeStep(StepExecution theStepExecution) { String jobUuid = theStepExecution.getJobExecution().getJobParameters().getString("jobUUID"); if (jobUuid != null) { - myBulkExportDaoSvc.setJobToStatus(jobUuid, BulkJobStatusEnum.BUILDING); + myBulkExportDaoSvc.setJobToStatus(jobUuid, BulkExportJobStatusEnum.BUILDING); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportGenerateResourceFilesStepListener.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportGenerateResourceFilesStepListener.java similarity index 88% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportGenerateResourceFilesStepListener.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportGenerateResourceFilesStepListener.java index cbd7e651762..699055e6404 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportGenerateResourceFilesStepListener.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportGenerateResourceFilesStepListener.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -20,8 +20,8 @@ package ca.uhn.fhir.jpa.bulk.job; * #L% */ -import ca.uhn.fhir.jpa.bulk.model.BulkJobStatusEnum; -import ca.uhn.fhir.jpa.bulk.svc.BulkExportDaoSvc; +import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum; +import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportDaoSvc; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepExecutionListener; @@ -55,7 +55,7 @@ public class BulkExportGenerateResourceFilesStepListener implements StepExecutio } assert isNotBlank(jobUuid); String exitDescription = theStepExecution.getExitStatus().getExitDescription(); - myBulkExportDaoSvc.setJobToStatus(jobUuid, BulkJobStatusEnum.ERROR, exitDescription); + myBulkExportDaoSvc.setJobToStatus(jobUuid, BulkExportJobStatusEnum.ERROR, exitDescription); } return theStepExecution.getExitStatus(); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobCloser.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportJobCloser.java similarity index 83% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobCloser.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportJobCloser.java index 6e336c16b54..291251894d3 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobCloser.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportJobCloser.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -20,8 +20,8 @@ package ca.uhn.fhir.jpa.bulk.job; * #L% */ -import ca.uhn.fhir.jpa.bulk.model.BulkJobStatusEnum; -import ca.uhn.fhir.jpa.bulk.svc.BulkExportDaoSvc; +import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum; +import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportDaoSvc; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.StepContribution; import org.springframework.batch.core.scope.context.ChunkContext; @@ -44,9 +44,9 @@ public class BulkExportJobCloser implements Tasklet { @Override public RepeatStatus execute(StepContribution theStepContribution, ChunkContext theChunkContext) { if (theChunkContext.getStepContext().getStepExecution().getJobExecution().getStatus() == BatchStatus.STARTED) { - myBulkExportDaoSvc.setJobToStatus(myJobUUID, BulkJobStatusEnum.COMPLETE); + myBulkExportDaoSvc.setJobToStatus(myJobUUID, BulkExportJobStatusEnum.COMPLETE); } else { - myBulkExportDaoSvc.setJobToStatus(myJobUUID, BulkJobStatusEnum.ERROR); + myBulkExportDaoSvc.setJobToStatus(myJobUUID, BulkExportJobStatusEnum.ERROR); } return RepeatStatus.FINISHED; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportJobConfig.java similarity index 88% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobConfig.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportJobConfig.java index 22a11abcbdc..6a44261b140 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportJobConfig.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -23,7 +23,7 @@ package ca.uhn.fhir.jpa.bulk.job; import ca.uhn.fhir.jpa.batch.BatchJobsConfig; import ca.uhn.fhir.jpa.batch.processors.GoldenResourceAnnotatingProcessor; import ca.uhn.fhir.jpa.batch.processors.PidToIBaseResourceProcessor; -import ca.uhn.fhir.jpa.bulk.svc.BulkExportDaoSvc; +import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportDaoSvc; import ca.uhn.fhir.jpa.dao.mdm.MdmExpansionCacheSvc; import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -35,8 +35,6 @@ import org.springframework.batch.core.configuration.annotation.JobScope; import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.item.ItemProcessor; -import org.springframework.batch.item.ItemReader; -import org.springframework.batch.item.ItemWriter; import org.springframework.batch.item.support.CompositeItemProcessor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -59,6 +57,7 @@ public class BulkExportJobConfig { public static final String GROUP_ID_PARAMETER = "groupId"; public static final String RESOURCE_TYPES_PARAMETER = "resourceTypes"; public static final int CHUNK_SIZE = 100; + public static final String JOB_DESCRIPTION = "jobDescription"; @Autowired private StepBuilderFactory myStepBuilderFactory; @@ -90,9 +89,9 @@ public class BulkExportJobConfig { @Lazy public Job bulkExportJob() { return myJobBuilderFactory.get(BatchJobsConfig.BULK_EXPORT_JOB_NAME) - .validator(bulkJobParameterValidator()) + .validator(bulkExportJobParameterValidator()) .start(createBulkExportEntityStep()) - .next(partitionStep()) + .next(bulkExportPartitionStep()) .next(closeJobStep()) .build(); } @@ -114,7 +113,7 @@ public class BulkExportJobConfig { public Job groupBulkExportJob() { return myJobBuilderFactory.get(BatchJobsConfig.GROUP_BULK_EXPORT_JOB_NAME) .validator(groupBulkJobParameterValidator()) - .validator(bulkJobParameterValidator()) + .validator(bulkExportJobParameterValidator()) .start(createBulkExportEntityStep()) .next(groupPartitionStep()) .next(closeJobStep()) @@ -125,7 +124,7 @@ public class BulkExportJobConfig { @Lazy public Job patientBulkExportJob() { return myJobBuilderFactory.get(BatchJobsConfig.PATIENT_BULK_EXPORT_JOB_NAME) - .validator(bulkJobParameterValidator()) + .validator(bulkExportJobParameterValidator()) .start(createBulkExportEntityStep()) .next(patientPartitionStep()) .next(closeJobStep()) @@ -150,8 +149,9 @@ public class BulkExportJobConfig { return new CreateBulkExportEntityTasklet(); } + @Bean - public JobParametersValidator bulkJobParameterValidator() { + public JobParametersValidator bulkExportJobParameterValidator() { return new BulkExportJobParameterValidator(); } @@ -159,7 +159,7 @@ public class BulkExportJobConfig { @Bean public Step groupBulkExportGenerateResourceFilesStep() { return myStepBuilderFactory.get("groupBulkExportGenerateResourceFilesStep") - ., List> chunk(CHUNK_SIZE) //1000 resources per generated file, as the reader returns 10 resources at a time. + ., List>chunk(CHUNK_SIZE) //1000 resources per generated file, as the reader returns 10 resources at a time. .reader(groupBulkItemReader()) .processor(inflateResourceThenAnnotateWithGoldenResourceProcessor()) .writer(resourceToFileWriter()) @@ -170,17 +170,18 @@ public class BulkExportJobConfig { @Bean public Step bulkExportGenerateResourceFilesStep() { return myStepBuilderFactory.get("bulkExportGenerateResourceFilesStep") - ., List> chunk(CHUNK_SIZE) //1000 resources per generated file, as the reader returns 10 resources at a time. + ., List>chunk(CHUNK_SIZE) //1000 resources per generated file, as the reader returns 10 resources at a time. .reader(bulkItemReader()) .processor(myPidToIBaseResourceProcessor) .writer(resourceToFileWriter()) .listener(bulkExportGenerateResourceFilesStepListener()) .build(); } + @Bean public Step patientBulkExportGenerateResourceFilesStep() { return myStepBuilderFactory.get("patientBulkExportGenerateResourceFilesStep") - ., List> chunk(CHUNK_SIZE) //1000 resources per generated file, as the reader returns 10 resources at a time. + ., List>chunk(CHUNK_SIZE) //1000 resources per generated file, as the reader returns 10 resources at a time. .reader(patientBulkItemReader()) .processor(myPidToIBaseResourceProcessor) .writer(resourceToFileWriter()) @@ -214,7 +215,7 @@ public class BulkExportJobConfig { } @Bean - public Step partitionStep() { + public Step bulkExportPartitionStep() { return myStepBuilderFactory.get("partitionStep") .partitioner("bulkExportGenerateResourceFilesStep", bulkExportResourceTypePartitioner()) .step(bulkExportGenerateResourceFilesStep()) @@ -240,7 +241,7 @@ public class BulkExportJobConfig { @Bean @StepScope - public GroupBulkItemReader groupBulkItemReader(){ + public GroupBulkItemReader groupBulkItemReader() { return new GroupBulkItemReader(); } @@ -252,7 +253,7 @@ public class BulkExportJobConfig { @Bean @StepScope - public BulkItemReader bulkItemReader(){ + public BulkItemReader bulkItemReader() { return new BulkItemReader(); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobParameterValidator.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportJobParameterValidator.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobParameterValidator.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportJobParameterValidator.java index 01e503c6687..64d06052d43 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobParameterValidator.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportJobParameterValidator.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -24,7 +24,6 @@ import ca.uhn.fhir.jpa.dao.data.IBulkExportJobDao; import ca.uhn.fhir.jpa.entity.BulkExportJobEntity; import ca.uhn.fhir.rest.api.Constants; import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; import org.springframework.batch.core.JobParameters; import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.JobParametersValidator; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobParametersBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportJobParametersBuilder.java similarity index 95% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobParametersBuilder.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportJobParametersBuilder.java index 7219c900c87..881e7215620 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkExportJobParametersBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkExportJobParametersBuilder.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.bulk.job; * #L% */ -import ca.uhn.fhir.jpa.bulk.api.BulkDataExportOptions; +import ca.uhn.fhir.jpa.bulk.export.api.BulkDataExportOptions; import ca.uhn.fhir.rest.api.Constants; import org.springframework.batch.core.JobParametersBuilder; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkItemReader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkItemReader.java similarity index 94% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkItemReader.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkItemReader.java index 0c9bf4fae2b..83f7c7d0d50 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/BulkItemReader.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/BulkItemReader.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -29,7 +29,6 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import org.slf4j.Logger; -import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -43,7 +42,7 @@ public class BulkItemReader extends BaseBulkItemReader { private static final Logger ourLog = Logs.getBatchTroubleshootingLog(); @Override - Iterator getResourcePidIterator() { + protected Iterator getResourcePidIterator() { ourLog.info("Bulk export assembling export of type {} for job {}", myResourceType, myJobUUID); Set myReadPids = new HashSet<>(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/CreateBulkExportEntityTasklet.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/CreateBulkExportEntityTasklet.java similarity index 93% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/CreateBulkExportEntityTasklet.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/CreateBulkExportEntityTasklet.java index 74e85d2188a..3a14e3e1145 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/CreateBulkExportEntityTasklet.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/CreateBulkExportEntityTasklet.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -20,8 +20,8 @@ package ca.uhn.fhir.jpa.bulk.job; * #L% */ -import ca.uhn.fhir.jpa.bulk.api.BulkDataExportOptions; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.api.BulkDataExportOptions; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.Constants; import org.apache.commons.lang3.StringUtils; @@ -87,7 +87,7 @@ public class CreateBulkExportEntityTasklet implements Tasklet { } } - private void addUUIDToJobContext(ChunkContext theChunkContext, String theJobUUID) { + public static void addUUIDToJobContext(ChunkContext theChunkContext, String theJobUUID) { theChunkContext .getStepContext() .getStepExecution() diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/GroupBulkExportJobParametersBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkExportJobParametersBuilder.java similarity index 96% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/GroupBulkExportJobParametersBuilder.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkExportJobParametersBuilder.java index f79adc79ee1..5d9b90f7004 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/GroupBulkExportJobParametersBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkExportJobParametersBuilder.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/GroupBulkItemReader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/GroupBulkItemReader.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java index 30a7567776e..3a10fec2aae 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/GroupBulkItemReader.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -36,7 +36,6 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; import ca.uhn.fhir.rest.param.ReferenceOrListParam; import ca.uhn.fhir.rest.param.ReferenceParam; -import com.google.common.collect.Multimaps; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; @@ -81,7 +80,7 @@ public class GroupBulkItemReader extends BaseBulkItemReader implements ItemReade private MdmExpansionCacheSvc myMdmExpansionCacheSvc; @Override - Iterator getResourcePidIterator() { + protected Iterator getResourcePidIterator() { Set myReadPids = new HashSet<>(); //Short circuit out if we detect we are attempting to extract patients diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/GroupIdPresentValidator.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupIdPresentValidator.java similarity index 93% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/GroupIdPresentValidator.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupIdPresentValidator.java index a28eaaf0338..8e662049b5c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/GroupIdPresentValidator.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupIdPresentValidator.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -26,7 +26,7 @@ import org.springframework.batch.core.JobParametersInvalidException; import org.springframework.batch.core.JobParametersValidator; -import static ca.uhn.fhir.jpa.bulk.job.BulkExportJobConfig.*; +import static ca.uhn.fhir.jpa.bulk.export.job.BulkExportJobConfig.*; import static org.slf4j.LoggerFactory.getLogger; public class GroupIdPresentValidator implements JobParametersValidator { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/PatientBulkItemReader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/PatientBulkItemReader.java similarity index 97% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/PatientBulkItemReader.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/PatientBulkItemReader.java index c206404ac95..93519863ec7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/PatientBulkItemReader.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/PatientBulkItemReader.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -61,7 +61,7 @@ public class PatientBulkItemReader extends BaseBulkItemReader implements ItemRea } @Override - Iterator getResourcePidIterator() { + protected Iterator getResourcePidIterator() { if (myDaoConfig.getIndexMissingFields() == DaoConfig.IndexEnabledEnum.DISABLED) { String errorMessage = "You attempted to start a Patient Bulk Export, but the system has `Index Missing Fields` disabled. It must be enabled for Patient Bulk Export"; ourLog.error(errorMessage); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/ResourceToFileWriter.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/ResourceToFileWriter.java similarity index 97% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/ResourceToFileWriter.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/ResourceToFileWriter.java index 82501c274c8..8b4ebe7e86a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/ResourceToFileWriter.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/ResourceToFileWriter.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -25,7 +25,7 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.batch.log.Logs; -import ca.uhn.fhir.jpa.bulk.svc.BulkExportDaoSvc; +import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportDaoSvc; import ca.uhn.fhir.jpa.entity.BulkExportCollectionFileEntity; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.Constants; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/ResourceTypePartitioner.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/ResourceTypePartitioner.java similarity index 96% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/ResourceTypePartitioner.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/ResourceTypePartitioner.java index 4cde1ab954b..7eb612d2211 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/job/ResourceTypePartitioner.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/ResourceTypePartitioner.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.job; +package ca.uhn.fhir.jpa.bulk.export.job; /*- * #%L @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.bulk.job; * #L% */ -import ca.uhn.fhir.jpa.bulk.svc.BulkExportDaoSvc; +import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportDaoSvc; import org.slf4j.Logger; import org.springframework.batch.core.partition.support.Partitioner; import org.springframework.batch.item.ExecutionContext; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/model/BulkJobStatusEnum.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/model/BulkExportJobStatusEnum.java similarity index 77% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/model/BulkJobStatusEnum.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/model/BulkExportJobStatusEnum.java index e4fe675665c..db520b9cfd2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/model/BulkJobStatusEnum.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/model/BulkExportJobStatusEnum.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.model; +package ca.uhn.fhir.jpa.bulk.export.model; /*- * #%L @@ -20,7 +20,14 @@ package ca.uhn.fhir.jpa.bulk.model; * #L% */ -public enum BulkJobStatusEnum { +import com.fasterxml.jackson.annotation.JsonFormat; + +@JsonFormat(shape = JsonFormat.Shape.STRING) +public enum BulkExportJobStatusEnum { + + /** + * Sorting OK! + */ SUBMITTED, BUILDING, diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/model/BulkExportResponseJson.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/model/BulkExportResponseJson.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/model/BulkExportResponseJson.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/model/BulkExportResponseJson.java index 011eb6ddf14..31f3daf32d7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/model/BulkExportResponseJson.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/model/BulkExportResponseJson.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.model; +package ca.uhn.fhir.jpa.bulk.export.model; /*- * #%L diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/provider/BulkDataExportProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/provider/BulkDataExportProvider.java similarity index 98% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/provider/BulkDataExportProvider.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/provider/BulkDataExportProvider.java index 28c4bc39e05..57ca30d0f78 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/provider/BulkDataExportProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/provider/BulkDataExportProvider.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.provider; +package ca.uhn.fhir.jpa.bulk.export.provider; /*- * #%L @@ -21,9 +21,9 @@ package ca.uhn.fhir.jpa.bulk.provider; */ import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.bulk.api.BulkDataExportOptions; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; -import ca.uhn.fhir.jpa.bulk.model.BulkExportResponseJson; +import ca.uhn.fhir.jpa.bulk.export.api.BulkDataExportOptions; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.model.BulkExportResponseJson; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/svc/BulkDataExportSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkDataExportSvcImpl.java similarity index 95% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/svc/BulkDataExportSvcImpl.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkDataExportSvcImpl.java index 7bec3d61a89..872c036f63a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/svc/BulkDataExportSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkDataExportSvcImpl.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.svc; +package ca.uhn.fhir.jpa.bulk.export.svc; /*- * #%L @@ -23,16 +23,15 @@ package ca.uhn.fhir.jpa.bulk.svc; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; -import ca.uhn.fhir.fhirpath.IFhirPath; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.model.ExpungeOptions; import ca.uhn.fhir.jpa.batch.BatchJobsConfig; import ca.uhn.fhir.jpa.batch.api.IBatchJobSubmitter; -import ca.uhn.fhir.jpa.bulk.api.BulkDataExportOptions; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; -import ca.uhn.fhir.jpa.bulk.job.BulkExportJobConfig; -import ca.uhn.fhir.jpa.bulk.model.BulkJobStatusEnum; +import ca.uhn.fhir.jpa.bulk.export.api.BulkDataExportOptions; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum; import ca.uhn.fhir.jpa.dao.data.IBulkExportCollectionDao; import ca.uhn.fhir.jpa.dao.data.IBulkExportCollectionFileDao; import ca.uhn.fhir.jpa.dao.data.IBulkExportJobDao; @@ -43,16 +42,12 @@ import ca.uhn.fhir.jpa.model.sched.HapiJob; import ca.uhn.fhir.jpa.model.sched.ISchedulerService; import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition; import ca.uhn.fhir.jpa.model.util.JpaConstants; -import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.util.UrlUtil; -import com.google.common.collect.Sets; import org.apache.commons.lang3.time.DateUtils; -import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBinary; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.InstantType; import org.quartz.JobExecutionContext; @@ -78,9 +73,9 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; -import static ca.uhn.fhir.jpa.bulk.api.BulkDataExportOptions.ExportStyle.GROUP; -import static ca.uhn.fhir.jpa.bulk.api.BulkDataExportOptions.ExportStyle.PATIENT; -import static ca.uhn.fhir.jpa.bulk.api.BulkDataExportOptions.ExportStyle.SYSTEM; +import static ca.uhn.fhir.jpa.bulk.export.api.BulkDataExportOptions.ExportStyle.GROUP; +import static ca.uhn.fhir.jpa.bulk.export.api.BulkDataExportOptions.ExportStyle.PATIENT; +import static ca.uhn.fhir.jpa.bulk.export.api.BulkDataExportOptions.ExportStyle.SYSTEM; import static ca.uhn.fhir.util.UrlUtil.escapeUrlParam; import static ca.uhn.fhir.util.UrlUtil.escapeUrlParams; import static org.apache.commons.lang3.StringUtils.isNotBlank; @@ -136,7 +131,7 @@ public class BulkDataExportSvcImpl implements IBulkDataExportSvc { Optional jobToProcessOpt = myTxTemplate.execute(t -> { Pageable page = PageRequest.of(0, 1); - Slice submittedJobs = myBulkExportJobDao.findByStatus(page, BulkJobStatusEnum.SUBMITTED); + Slice submittedJobs = myBulkExportJobDao.findByStatus(page, BulkExportJobStatusEnum.SUBMITTED); if (submittedJobs.isEmpty()) { return Optional.empty(); } @@ -158,7 +153,7 @@ public class BulkDataExportSvcImpl implements IBulkDataExportSvc { Optional submittedJobs = myBulkExportJobDao.findByJobId(jobUuid); if (submittedJobs.isPresent()) { BulkExportJobEntity jobEntity = submittedJobs.get(); - jobEntity.setStatus(BulkJobStatusEnum.ERROR); + jobEntity.setStatus(BulkExportJobStatusEnum.ERROR); jobEntity.setStatusMessage(e.getMessage()); myBulkExportJobDao.save(jobEntity); } @@ -344,7 +339,7 @@ public class BulkDataExportSvcImpl implements IBulkDataExportSvc { if (useCache) { Date cutoff = DateUtils.addMilliseconds(new Date(), -myReuseBulkExportForMillis); Pageable page = PageRequest.of(0, 10); - Slice existing = myBulkExportJobDao.findExistingJob(page, request, cutoff, BulkJobStatusEnum.ERROR); + Slice existing = myBulkExportJobDao.findExistingJob(page, request, cutoff, BulkExportJobStatusEnum.ERROR); if (!existing.isEmpty()) { return toSubmittedJobInfo(existing.iterator().next()); } @@ -373,7 +368,7 @@ public class BulkDataExportSvcImpl implements IBulkDataExportSvc { BulkExportJobEntity job = new BulkExportJobEntity(); job.setJobId(UUID.randomUUID().toString()); - job.setStatus(BulkJobStatusEnum.SUBMITTED); + job.setStatus(BulkExportJobStatusEnum.SUBMITTED); job.setSince(since); job.setCreated(new Date()); job.setRequest(request); @@ -445,7 +440,7 @@ public class BulkDataExportSvcImpl implements IBulkDataExportSvc { retVal.setStatusMessage(job.getStatusMessage()); retVal.setRequest(job.getRequest()); - if (job.getStatus() == BulkJobStatusEnum.COMPLETE) { + if (job.getStatus() == BulkExportJobStatusEnum.COMPLETE) { for (BulkExportCollectionEntity nextCollection : job.getCollections()) { for (BulkExportCollectionFileEntity nextFile : nextCollection.getFiles()) { retVal.addFile() diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/svc/BulkExportCollectionFileDaoSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkExportCollectionFileDaoSvc.java similarity index 96% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/svc/BulkExportCollectionFileDaoSvc.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkExportCollectionFileDaoSvc.java index 268cd4c29e6..cc255829231 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/svc/BulkExportCollectionFileDaoSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkExportCollectionFileDaoSvc.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.svc; +package ca.uhn.fhir.jpa.bulk.export.svc; /*- * #%L diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/svc/BulkExportDaoSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkExportDaoSvc.java similarity index 90% rename from hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/svc/BulkExportDaoSvc.java rename to hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkExportDaoSvc.java index d69f8cbc235..7aa6521b68e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/svc/BulkExportDaoSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkExportDaoSvc.java @@ -1,4 +1,4 @@ -package ca.uhn.fhir.jpa.bulk.svc; +package ca.uhn.fhir.jpa.bulk.export.svc; /*- * #%L @@ -20,9 +20,7 @@ package ca.uhn.fhir.jpa.bulk.svc; * #L% */ -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -import ca.uhn.fhir.jpa.bulk.model.BulkJobStatusEnum; +import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum; import ca.uhn.fhir.jpa.dao.data.IBulkExportCollectionDao; import ca.uhn.fhir.jpa.dao.data.IBulkExportCollectionFileDao; import ca.uhn.fhir.jpa.dao.data.IBulkExportJobDao; @@ -84,12 +82,12 @@ public class BulkExportDaoSvc { } @Transactional - public void setJobToStatus(String theJobUUID, BulkJobStatusEnum theStatus) { + public void setJobToStatus(String theJobUUID, BulkExportJobStatusEnum theStatus) { setJobToStatus(theJobUUID, theStatus, null); } @Transactional - public void setJobToStatus(String theJobUUID, BulkJobStatusEnum theStatus, String theStatusMessage) { + public void setJobToStatus(String theJobUUID, BulkExportJobStatusEnum theStatus, String theStatusMessage) { Optional oJob = myBulkExportJobDao.findByJobId(theJobUUID); if (!oJob.isPresent()) { ourLog.error("Job with UUID {} doesn't exist!", theJobUUID); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/api/IBulkDataImportSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/api/IBulkDataImportSvc.java new file mode 100644 index 00000000000..7e6ef86b6ad --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/api/IBulkDataImportSvc.java @@ -0,0 +1,93 @@ +package ca.uhn.fhir.jpa.bulk.imprt.api; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.bulk.imprt.model.BulkImportJobFileJson; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobJson; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum; + +import javax.annotation.Nonnull; +import java.util.List; + +public interface IBulkDataImportSvc { + + /** + * Create a new job in {@link ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum#STAGING STAGING} state (meaning it won't yet be worked on and can be added to) + */ + String createNewJob(BulkImportJobJson theJobDescription, @Nonnull List theInitialFiles); + + /** + * Add more files to a job in {@link ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum#STAGING STAGING} state + * + * @param theJobId The job ID + * @param theFiles The files to add to the job + */ + void addFilesToJob(String theJobId, List theFiles); + + /** + * Move a job from {@link ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum#STAGING STAGING} + * state to {@link ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum#READY READY} + * state, meaning that is is a candidate to be picked up for processing + * + * @param theJobId The job ID + */ + void markJobAsReadyForActivation(String theJobId); + + /** + * This method is intended to be called from the job scheduler, and will begin execution on + * the next job in status {@link ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum#READY READY} + * + * @return Returns {@literal true} if a job was activated + */ + boolean activateNextReadyJob(); + + /** + * Updates the job status for the given job + */ + void setJobToStatus(String theJobId, BulkImportJobStatusEnum theStatus); + + /** + * Updates the job status for the given job + */ + void setJobToStatus(String theJobId, BulkImportJobStatusEnum theStatus, String theStatusMessage); + + /** + * Gets the number of files available for a given Job ID + * + * @param theJobId The job ID + * @return The file count + */ + BulkImportJobJson fetchJob(String theJobId); + + /** + * Fetch a given file by job ID + * + * @param theJobId The job ID + * @param theFileIndex The index of the file within the job + * @return The file + */ + BulkImportJobFileJson fetchFile(String theJobId, int theFileIndex); + + /** + * Delete all input files associated with a particular job + */ + void deleteJobFiles(String theJobId); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/ActivateBulkImportEntityStepListener.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/ActivateBulkImportEntityStepListener.java new file mode 100644 index 00000000000..4e842856773 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/ActivateBulkImportEntityStepListener.java @@ -0,0 +1,51 @@ +package ca.uhn.fhir.jpa.bulk.imprt.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.bulk.export.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.bulk.imprt.api.IBulkDataImportSvc; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Will run before and after a job to set the status to whatever is appropriate. + */ +public class ActivateBulkImportEntityStepListener implements StepExecutionListener { + + @Autowired + private IBulkDataImportSvc myBulkImportDaoSvc; + + @Override + public void beforeStep(StepExecution theStepExecution) { + String jobUuid = theStepExecution.getJobExecution().getJobParameters().getString(BulkExportJobConfig.JOB_UUID_PARAMETER); + if (jobUuid != null) { + myBulkImportDaoSvc.setJobToStatus(jobUuid, BulkImportJobStatusEnum.RUNNING); + } + } + + @Override + public ExitStatus afterStep(StepExecution theStepExecution) { + return ExitStatus.EXECUTING; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportFileReader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportFileReader.java new file mode 100644 index 00000000000..601e76244f0 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportFileReader.java @@ -0,0 +1,76 @@ +package ca.uhn.fhir.jpa.bulk.imprt.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.FhirContext; +import ca.uhn.fhir.jpa.batch.log.Logs; +import ca.uhn.fhir.jpa.bulk.export.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.bulk.imprt.api.IBulkDataImportSvc; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobFileJson; +import ca.uhn.fhir.jpa.bulk.imprt.model.ParsedBulkImportRecord; +import ca.uhn.fhir.util.IoUtil; +import com.google.common.io.LineReader; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.springframework.batch.item.ItemReader; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import java.io.StringReader; + +@SuppressWarnings("UnstableApiUsage") +public class BulkImportFileReader implements ItemReader { + + @Autowired + private IBulkDataImportSvc myBulkDataImportSvc; + @Autowired + private FhirContext myFhirContext; + @Value("#{stepExecutionContext['" + BulkExportJobConfig.JOB_UUID_PARAMETER + "']}") + private String myJobUuid; + @Value("#{stepExecutionContext['" + BulkImportPartitioner.FILE_INDEX + "']}") + private int myFileIndex; + + private StringReader myReader; + private LineReader myLineReader; + private int myLineIndex; + private String myTenantName; + + @Override + public ParsedBulkImportRecord read() throws Exception { + + if (myReader == null) { + BulkImportJobFileJson file = myBulkDataImportSvc.fetchFile(myJobUuid, myFileIndex); + myTenantName = file.getTenantName(); + myReader = new StringReader(file.getContents()); + myLineReader = new LineReader(myReader); + } + + String nextLine = myLineReader.readLine(); + if (nextLine == null) { + IoUtil.closeQuietly(myReader); + return null; + } + + Logs.getBatchTroubleshootingLog().debug("Reading line {} file index {} for job: {}", myLineIndex++, myFileIndex, myJobUuid); + + IBaseResource parsed = myFhirContext.newJsonParser().parseResource(nextLine); + return new ParsedBulkImportRecord(myTenantName, parsed); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportFileWriter.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportFileWriter.java new file mode 100644 index 00000000000..5f893474c26 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportFileWriter.java @@ -0,0 +1,74 @@ +package ca.uhn.fhir.jpa.bulk.imprt.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.api.dao.DaoRegistry; +import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.bulk.export.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.bulk.imprt.model.JobFileRowProcessingModeEnum; +import ca.uhn.fhir.jpa.bulk.imprt.model.ParsedBulkImportRecord; +import ca.uhn.fhir.jpa.partition.SystemRequestDetails; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.item.ItemWriter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import java.util.List; + +public class BulkImportFileWriter implements ItemWriter { + + private static final Logger ourLog = LoggerFactory.getLogger(BulkImportFileWriter.class); + @Value("#{stepExecutionContext['" + BulkExportJobConfig.JOB_UUID_PARAMETER + "']}") + private String myJobUuid; + @Value("#{stepExecutionContext['" + BulkImportPartitioner.FILE_INDEX + "']}") + private int myFileIndex; + @Value("#{stepExecutionContext['" + BulkImportPartitioner.ROW_PROCESSING_MODE + "']}") + private JobFileRowProcessingModeEnum myRowProcessingMode; + @Autowired + private DaoRegistry myDaoRegistry; + + @SuppressWarnings({"SwitchStatementWithTooFewBranches", "rawtypes", "unchecked"}) + @Override + public void write(List theItemLists) throws Exception { + ourLog.info("Beginning bulk import write {} chunks Job[{}] FileIndex[{}]", theItemLists.size(), myJobUuid, myFileIndex); + + for (ParsedBulkImportRecord nextItem : theItemLists) { + + SystemRequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setTenantId(nextItem.getTenantName()); + + // Yeah this is a lame switch - We'll add more later I swear + switch (myRowProcessingMode) { + default: + case FHIR_TRANSACTION: + IFhirSystemDao systemDao = myDaoRegistry.getSystemDao(); + IBaseResource inputBundle = nextItem.getRowContent(); + systemDao.transactionNested(requestDetails, inputBundle); + break; + } + + } + + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportJobCloser.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportJobCloser.java new file mode 100644 index 00000000000..504874e327d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportJobCloser.java @@ -0,0 +1,57 @@ +package ca.uhn.fhir.jpa.bulk.imprt.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.bulk.export.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.bulk.imprt.api.IBulkDataImportSvc; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +/** + * Will run before and after a job to set the status to whatever is appropriate. + */ +public class BulkImportJobCloser implements Tasklet { + + @Value("#{jobParameters['" + BulkExportJobConfig.JOB_UUID_PARAMETER + "']}") + private String myJobUUID; + + @Autowired + private IBulkDataImportSvc myBulkDataImportSvc; + + @Override + public RepeatStatus execute(StepContribution theStepContribution, ChunkContext theChunkContext) { + BatchStatus executionStatus = theChunkContext.getStepContext().getStepExecution().getJobExecution().getStatus(); + if (executionStatus == BatchStatus.STARTED) { + myBulkDataImportSvc.setJobToStatus(myJobUUID, BulkImportJobStatusEnum.COMPLETE); + myBulkDataImportSvc.deleteJobFiles(myJobUUID); + } else { + myBulkDataImportSvc.setJobToStatus(myJobUUID, BulkImportJobStatusEnum.ERROR, "Found job in status: " + executionStatus); + myBulkDataImportSvc.deleteJobFiles(myJobUUID); + } + return RepeatStatus.FINISHED; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportJobConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportJobConfig.java new file mode 100644 index 00000000000..fd86a8ff3ab --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportJobConfig.java @@ -0,0 +1,169 @@ +package ca.uhn.fhir.jpa.bulk.imprt.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.batch.BatchConstants; +import ca.uhn.fhir.jpa.bulk.imprt.model.ParsedBulkImportRecord; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParametersValidator; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.JobBuilderFactory; +import org.springframework.batch.core.configuration.annotation.JobScope; +import org.springframework.batch.core.configuration.annotation.StepBuilderFactory; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.partition.PartitionHandler; +import org.springframework.batch.core.partition.support.TaskExecutorPartitionHandler; +import org.springframework.batch.item.ItemWriter; +import org.springframework.batch.repeat.CompletionPolicy; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.task.TaskExecutor; + +import static ca.uhn.fhir.jpa.batch.BatchJobsConfig.BULK_IMPORT_JOB_NAME; + +/** + * Spring batch Job configuration file. Contains all necessary plumbing to run a + * Bulk Export job. + */ +@Configuration +public class BulkImportJobConfig { + + public static final String JOB_PARAM_COMMIT_INTERVAL = "commitInterval"; + + @Autowired + private StepBuilderFactory myStepBuilderFactory; + + @Autowired + private JobBuilderFactory myJobBuilderFactory; + + @Autowired + @Qualifier(BatchConstants.JOB_LAUNCHING_TASK_EXECUTOR) + private TaskExecutor myTaskExecutor; + + @Bean(name = BULK_IMPORT_JOB_NAME) + @Lazy + public Job bulkImportJob() throws Exception { + return myJobBuilderFactory.get(BULK_IMPORT_JOB_NAME) + .validator(bulkImportJobParameterValidator()) + .start(bulkImportPartitionStep()) + .next(bulkImportCloseJobStep()) + .build(); + } + + @Bean + public JobParametersValidator bulkImportJobParameterValidator() { + return new BulkImportJobParameterValidator(); + } + + @Bean + public CreateBulkImportEntityTasklet createBulkImportEntityTasklet() { + return new CreateBulkImportEntityTasklet(); + } + + @Bean + @JobScope + public ActivateBulkImportEntityStepListener activateBulkImportEntityStepListener() { + return new ActivateBulkImportEntityStepListener(); + } + + @Bean + public Step bulkImportPartitionStep() throws Exception { + return myStepBuilderFactory.get("bulkImportPartitionStep") + .partitioner("bulkImportPartitionStep", bulkImportPartitioner()) + .partitionHandler(partitionHandler()) + .listener(activateBulkImportEntityStepListener()) + .gridSize(10) + .build(); + } + + private PartitionHandler partitionHandler() throws Exception { + assert myTaskExecutor != null; + + TaskExecutorPartitionHandler retVal = new TaskExecutorPartitionHandler(); + retVal.setStep(bulkImportProcessFilesStep()); + retVal.setTaskExecutor(myTaskExecutor); + retVal.afterPropertiesSet(); + return retVal; + } + + @Bean + public Step bulkImportCloseJobStep() { + return myStepBuilderFactory.get("bulkImportCloseJobStep") + .tasklet(bulkImportJobCloser()) + .build(); + } + + @Bean + @JobScope + public BulkImportJobCloser bulkImportJobCloser() { + return new BulkImportJobCloser(); + } + + @Bean + @JobScope + public BulkImportPartitioner bulkImportPartitioner() { + return new BulkImportPartitioner(); + } + + + @Bean + public Step bulkImportProcessFilesStep() { + CompletionPolicy completionPolicy = completionPolicy(); + + return myStepBuilderFactory.get("bulkImportProcessFilesStep") + .chunk(completionPolicy) + .reader(bulkImportFileReader()) + .writer(bulkImportFileWriter()) + .listener(bulkImportStepListener()) + .listener(completionPolicy) + .build(); + } + + @Bean + @StepScope + public CompletionPolicy completionPolicy() { + return new BulkImportProcessStepCompletionPolicy(); + } + + @Bean + @StepScope + public ItemWriter bulkImportFileWriter() { + return new BulkImportFileWriter(); + } + + + @Bean + @StepScope + public BulkImportFileReader bulkImportFileReader() { + return new BulkImportFileReader(); + } + + @Bean + @StepScope + public BulkImportStepListener bulkImportStepListener() { + return new BulkImportStepListener(); + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportJobParameterValidator.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportJobParameterValidator.java new file mode 100644 index 00000000000..a46405fec31 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportJobParameterValidator.java @@ -0,0 +1,70 @@ +package ca.uhn.fhir.jpa.bulk.imprt.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.bulk.export.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.dao.data.IBulkImportJobDao; +import ca.uhn.fhir.jpa.entity.BulkImportJobEntity; +import org.apache.commons.lang3.StringUtils; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.batch.core.JobParametersValidator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.Optional; + +/** + * This class will prevent a job from running if the UUID does not exist or is invalid. + */ +public class BulkImportJobParameterValidator implements JobParametersValidator { + + @Autowired + private IBulkImportJobDao myBulkImportJobDao; + @Autowired + private PlatformTransactionManager myTransactionManager; + + @Override + public void validate(JobParameters theJobParameters) throws JobParametersInvalidException { + if (theJobParameters == null) { + throw new JobParametersInvalidException("This job needs Parameters: [jobUUID]"); + } + + TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager); + String errorMessage = txTemplate.execute(tx -> { + StringBuilder errorBuilder = new StringBuilder(); + String jobUUID = theJobParameters.getString(BulkExportJobConfig.JOB_UUID_PARAMETER); + Optional oJob = myBulkImportJobDao.findByJobId(jobUUID); + if (!StringUtils.isBlank(jobUUID) && !oJob.isPresent()) { + errorBuilder.append("There is no persisted job that exists with UUID: "); + errorBuilder.append(jobUUID); + errorBuilder.append(". "); + } + + return errorBuilder.toString(); + }); + + if (!StringUtils.isEmpty(errorMessage)) { + throw new JobParametersInvalidException(errorMessage); + } + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportPartitioner.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportPartitioner.java new file mode 100644 index 00000000000..626c8caa016 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportPartitioner.java @@ -0,0 +1,72 @@ +package ca.uhn.fhir.jpa.bulk.imprt.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.bulk.export.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.bulk.imprt.api.IBulkDataImportSvc; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobJson; +import org.slf4j.Logger; +import org.springframework.batch.core.partition.support.Partitioner; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import javax.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; + +import static org.slf4j.LoggerFactory.getLogger; + +public class BulkImportPartitioner implements Partitioner { + public static final String FILE_INDEX = "fileIndex"; + public static final String ROW_PROCESSING_MODE = "rowProcessingMode"; + + private static final Logger ourLog = getLogger(BulkImportPartitioner.class); + + @Value("#{jobParameters['" + BulkExportJobConfig.JOB_UUID_PARAMETER + "']}") + private String myJobUUID; + + @Autowired + private IBulkDataImportSvc myBulkDataImportSvc; + + @Nonnull + @Override + public Map partition(int gridSize) { + Map retVal = new HashMap<>(); + + BulkImportJobJson job = myBulkDataImportSvc.fetchJob(myJobUUID); + + for (int i = 0; i < job.getFileCount(); i++) { + + ExecutionContext context = new ExecutionContext(); + context.putString(BulkExportJobConfig.JOB_UUID_PARAMETER, myJobUUID); + context.putInt(FILE_INDEX, i); + context.put(ROW_PROCESSING_MODE, job.getProcessingMode()); + + String key = "FILE" + i; + retVal.put(key, context); + } + + return retVal; + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportProcessStepCompletionPolicy.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportProcessStepCompletionPolicy.java new file mode 100644 index 00000000000..3a3afefc636 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportProcessStepCompletionPolicy.java @@ -0,0 +1,41 @@ +package ca.uhn.fhir.jpa.bulk.imprt.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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 org.springframework.batch.repeat.RepeatContext; +import org.springframework.batch.repeat.policy.CompletionPolicySupport; +import org.springframework.beans.factory.annotation.Value; + +import static ca.uhn.fhir.jpa.bulk.imprt.job.BulkImportJobConfig.JOB_PARAM_COMMIT_INTERVAL; + +public class BulkImportProcessStepCompletionPolicy extends CompletionPolicySupport { + + @Value("#{jobParameters['" + JOB_PARAM_COMMIT_INTERVAL + "']}") + private int myChunkSize; + + @Override + public boolean isComplete(RepeatContext context) { + if (context.getStartedCount() < myChunkSize) { + return false; + } + return true; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportStepListener.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportStepListener.java new file mode 100644 index 00000000000..8cb1c9b2693 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/BulkImportStepListener.java @@ -0,0 +1,63 @@ +package ca.uhn.fhir.jpa.bulk.imprt.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.bulk.export.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.bulk.imprt.api.IBulkDataImportSvc; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum; +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.StepExecution; +import org.springframework.batch.core.StepExecutionListener; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.annotation.Nonnull; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +/** + * This class sets the job status to ERROR if any failures occur while actually + * generating the export files. + */ +public class BulkImportStepListener implements StepExecutionListener { + + @Autowired + private IBulkDataImportSvc myBulkDataImportSvc; + + @Override + public void beforeStep(@Nonnull StepExecution stepExecution) { + // nothing + } + + @Override + public ExitStatus afterStep(StepExecution theStepExecution) { + if (theStepExecution.getExitStatus().getExitCode().equals(ExitStatus.FAILED.getExitCode())) { + //Try to fetch it from the parameters first, and if it doesn't exist, fetch it from the context. + String jobUuid = theStepExecution.getJobExecution().getJobParameters().getString(BulkExportJobConfig.JOB_UUID_PARAMETER); + if (jobUuid == null) { + jobUuid = theStepExecution.getJobExecution().getExecutionContext().getString(BulkExportJobConfig.JOB_UUID_PARAMETER); + } + assert isNotBlank(jobUuid); + String exitDescription = theStepExecution.getExitStatus().getExitDescription(); + myBulkDataImportSvc.setJobToStatus(jobUuid, BulkImportJobStatusEnum.ERROR, exitDescription); + } + return theStepExecution.getExitStatus(); + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/CreateBulkImportEntityTasklet.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/CreateBulkImportEntityTasklet.java new file mode 100644 index 00000000000..c543ba4961f --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/job/CreateBulkImportEntityTasklet.java @@ -0,0 +1,45 @@ +package ca.uhn.fhir.jpa.bulk.imprt.job; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.bulk.export.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.bulk.export.job.CreateBulkExportEntityTasklet; +import ca.uhn.fhir.util.ValidateUtil; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; + +import java.util.Map; + +public class CreateBulkImportEntityTasklet implements Tasklet { + + @Override + public RepeatStatus execute(StepContribution theStepContribution, ChunkContext theChunkContext) throws Exception { + Map jobParameters = theChunkContext.getStepContext().getJobParameters(); + + //We can leave early if they provided us with an existing job. + ValidateUtil.isTrueOrThrowInvalidRequest(jobParameters.containsKey(BulkExportJobConfig.JOB_UUID_PARAMETER), "Job doesn't have a UUID"); + CreateBulkExportEntityTasklet.addUUIDToJobContext(theChunkContext, (String) jobParameters.get(BulkExportJobConfig.JOB_UUID_PARAMETER)); + return RepeatStatus.FINISHED; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/BulkImportJobFileJson.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/BulkImportJobFileJson.java new file mode 100644 index 00000000000..fb215a2fdce --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/BulkImportJobFileJson.java @@ -0,0 +1,51 @@ +package ca.uhn.fhir.jpa.bulk.imprt.model; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.model.api.IModelJson; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class BulkImportJobFileJson implements IModelJson { + + @JsonProperty("tenantName") + private String myTenantName; + @JsonProperty("contents") + private String myContents; + + public String getTenantName() { + return myTenantName; + } + + public BulkImportJobFileJson setTenantName(String theTenantName) { + myTenantName = theTenantName; + return this; + } + + public String getContents() { + return myContents; + } + + public BulkImportJobFileJson setContents(String theContents) { + myContents = theContents; + return this; + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/BulkImportJobJson.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/BulkImportJobJson.java new file mode 100644 index 00000000000..fe6ea10d0ba --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/BulkImportJobJson.java @@ -0,0 +1,72 @@ +package ca.uhn.fhir.jpa.bulk.imprt.model; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.model.api.IModelJson; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class BulkImportJobJson implements IModelJson { + + @JsonProperty("processingMode") + private JobFileRowProcessingModeEnum myProcessingMode; + @JsonProperty("jobDescription") + private String myJobDescription; + @JsonProperty("fileCount") + private int myFileCount; + @JsonProperty("batchSize") + private int myBatchSize; + + public String getJobDescription() { + return myJobDescription; + } + + public BulkImportJobJson setJobDescription(String theJobDescription) { + myJobDescription = theJobDescription; + return this; + } + + public JobFileRowProcessingModeEnum getProcessingMode() { + return myProcessingMode; + } + + public BulkImportJobJson setProcessingMode(JobFileRowProcessingModeEnum theProcessingMode) { + myProcessingMode = theProcessingMode; + return this; + } + + public int getFileCount() { + return myFileCount; + } + + public BulkImportJobJson setFileCount(int theFileCount) { + myFileCount = theFileCount; + return this; + } + + public int getBatchSize() { + return myBatchSize; + } + + public BulkImportJobJson setBatchSize(int theBatchSize) { + myBatchSize = theBatchSize; + return this; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/BulkImportJobStatusEnum.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/BulkImportJobStatusEnum.java new file mode 100644 index 00000000000..5c3fe355224 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/BulkImportJobStatusEnum.java @@ -0,0 +1,34 @@ +package ca.uhn.fhir.jpa.bulk.imprt.model; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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 com.fasterxml.jackson.annotation.JsonFormat; + +@JsonFormat(shape = JsonFormat.Shape.STRING) +public enum BulkImportJobStatusEnum { + + STAGING, + READY, + RUNNING, + COMPLETE, + ERROR + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/JobFileRowProcessingModeEnum.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/JobFileRowProcessingModeEnum.java new file mode 100644 index 00000000000..92826d97242 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/JobFileRowProcessingModeEnum.java @@ -0,0 +1,34 @@ +package ca.uhn.fhir.jpa.bulk.imprt.model; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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 com.fasterxml.jackson.annotation.JsonFormat; + +@JsonFormat(shape = JsonFormat.Shape.STRING) +public enum JobFileRowProcessingModeEnum { + + /** + * Sorting OK + */ + + FHIR_TRANSACTION + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/ParsedBulkImportRecord.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/ParsedBulkImportRecord.java new file mode 100644 index 00000000000..fba884734c0 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/model/ParsedBulkImportRecord.java @@ -0,0 +1,46 @@ +package ca.uhn.fhir.jpa.bulk.imprt.model; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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 org.hl7.fhir.instance.model.api.IBaseResource; + +import java.io.Serializable; + +public class ParsedBulkImportRecord implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String myTenantName; + private final IBaseResource myRowContent; + + public ParsedBulkImportRecord(String theTenantName, IBaseResource theRowContent) { + myTenantName = theTenantName; + myRowContent = theRowContent; + } + + public String getTenantName() { + return myTenantName; + } + + public IBaseResource getRowContent() { + return myRowContent; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImpl.java new file mode 100644 index 00000000000..2bbb97abe5d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImpl.java @@ -0,0 +1,280 @@ +package ca.uhn.fhir.jpa.bulk.imprt.svc; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.batch.BatchJobsConfig; +import ca.uhn.fhir.jpa.batch.api.IBatchJobSubmitter; +import ca.uhn.fhir.jpa.bulk.export.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.bulk.imprt.api.IBulkDataImportSvc; +import ca.uhn.fhir.jpa.bulk.imprt.job.BulkImportJobConfig; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobFileJson; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobJson; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum; +import ca.uhn.fhir.jpa.dao.data.IBulkImportJobDao; +import ca.uhn.fhir.jpa.dao.data.IBulkImportJobFileDao; +import ca.uhn.fhir.jpa.entity.BulkImportJobEntity; +import ca.uhn.fhir.jpa.entity.BulkImportJobFileEntity; +import ca.uhn.fhir.jpa.model.sched.HapiJob; +import ca.uhn.fhir.jpa.model.sched.ISchedulerService; +import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.util.ValidateUtil; +import org.apache.commons.lang3.time.DateUtils; +import org.quartz.JobExecutionContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.JobParametersInvalidException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import javax.transaction.Transactional; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +public class BulkDataImportSvcImpl implements IBulkDataImportSvc { + private static final Logger ourLog = LoggerFactory.getLogger(BulkDataImportSvcImpl.class); + @Autowired + private IBulkImportJobDao myJobDao; + + @Autowired + private IBulkImportJobFileDao myJobFileDao; + @Autowired + private PlatformTransactionManager myTxManager; + private TransactionTemplate myTxTemplate; + @Autowired + private ISchedulerService mySchedulerService; + @Autowired + private IBatchJobSubmitter myJobSubmitter; + @Autowired + @Qualifier(BatchJobsConfig.BULK_IMPORT_JOB_NAME) + private org.springframework.batch.core.Job myBulkImportJob; + + @PostConstruct + public void start() { + myTxTemplate = new TransactionTemplate(myTxManager); + + ScheduledJobDefinition jobDetail = new ScheduledJobDefinition(); + jobDetail.setId(ActivationJob.class.getName()); + jobDetail.setJobClass(ActivationJob.class); + mySchedulerService.scheduleClusteredJob(10 * DateUtils.MILLIS_PER_SECOND, jobDetail); + } + + @Override + @Transactional + public String createNewJob(BulkImportJobJson theJobDescription, @Nonnull List theInitialFiles) { + ValidateUtil.isNotNullOrThrowUnprocessableEntity(theJobDescription, "Job must not be null"); + ValidateUtil.isNotNullOrThrowUnprocessableEntity(theJobDescription.getProcessingMode(), "Job File Processing mode must not be null"); + ValidateUtil.isTrueOrThrowInvalidRequest(theJobDescription.getBatchSize() > 0, "Job File Batch Size must be > 0"); + + String jobId = UUID.randomUUID().toString(); + + ourLog.info("Creating new Bulk Import job with {} files, assigning job ID: {}", theInitialFiles.size(), jobId); + + BulkImportJobEntity job = new BulkImportJobEntity(); + job.setJobId(jobId); + job.setFileCount(theInitialFiles.size()); + job.setStatus(BulkImportJobStatusEnum.STAGING); + job.setJobDescription(theJobDescription.getJobDescription()); + job.setBatchSize(theJobDescription.getBatchSize()); + job.setRowProcessingMode(theJobDescription.getProcessingMode()); + job = myJobDao.save(job); + + int nextSequence = 0; + addFilesToJob(theInitialFiles, job, nextSequence); + + return jobId; + } + + @Override + @Transactional + public void addFilesToJob(String theJobId, List theFiles) { + ourLog.info("Adding {} files to bulk import job: {}", theFiles.size(), theJobId); + + BulkImportJobEntity job = findJobByJobId(theJobId); + + ValidateUtil.isTrueOrThrowInvalidRequest(job.getStatus() == BulkImportJobStatusEnum.STAGING, "Job %s has status %s and can not be added to", theJobId, job.getStatus()); + + addFilesToJob(theFiles, job, job.getFileCount()); + + job.setFileCount(job.getFileCount() + theFiles.size()); + myJobDao.save(job); + } + + private BulkImportJobEntity findJobByJobId(String theJobId) { + BulkImportJobEntity job = myJobDao + .findByJobId(theJobId) + .orElseThrow(() -> new InvalidRequestException("Unknown job ID: " + theJobId)); + return job; + } + + @Override + @Transactional + public void markJobAsReadyForActivation(String theJobId) { + ourLog.info("Activating bulk import job {}", theJobId); + + BulkImportJobEntity job = findJobByJobId(theJobId); + ValidateUtil.isTrueOrThrowInvalidRequest(job.getStatus() == BulkImportJobStatusEnum.STAGING, "Bulk import job %s can not be activated in status: %s", theJobId, job.getStatus()); + + job.setStatus(BulkImportJobStatusEnum.READY); + myJobDao.save(job); + } + + /** + * To be called by the job scheduler + */ + @Transactional(value = Transactional.TxType.NEVER) + @Override + public boolean activateNextReadyJob() { + + Optional jobToProcessOpt = Objects.requireNonNull(myTxTemplate.execute(t -> { + Pageable page = PageRequest.of(0, 1); + Slice submittedJobs = myJobDao.findByStatus(page, BulkImportJobStatusEnum.READY); + if (submittedJobs.isEmpty()) { + return Optional.empty(); + } + return Optional.of(submittedJobs.getContent().get(0)); + })); + + if (!jobToProcessOpt.isPresent()) { + return false; + } + + BulkImportJobEntity bulkImportJobEntity = jobToProcessOpt.get(); + + String jobUuid = bulkImportJobEntity.getJobId(); + try { + processJob(bulkImportJobEntity); + } catch (Exception e) { + ourLog.error("Failure while preparing bulk export extract", e); + myTxTemplate.execute(t -> { + Optional submittedJobs = myJobDao.findByJobId(jobUuid); + if (submittedJobs.isPresent()) { + BulkImportJobEntity jobEntity = submittedJobs.get(); + jobEntity.setStatus(BulkImportJobStatusEnum.ERROR); + jobEntity.setStatusMessage(e.getMessage()); + myJobDao.save(jobEntity); + } + return false; + }); + } + + return true; + } + + @Override + @Transactional + public void setJobToStatus(String theJobId, BulkImportJobStatusEnum theStatus) { + setJobToStatus(theJobId, theStatus, null); + } + + @Override + public void setJobToStatus(String theJobId, BulkImportJobStatusEnum theStatus, String theStatusMessage) { + BulkImportJobEntity job = findJobByJobId(theJobId); + job.setStatus(theStatus); + job.setStatusMessage(theStatusMessage); + myJobDao.save(job); + } + + @Override + @Transactional + public BulkImportJobJson fetchJob(String theJobId) { + BulkImportJobEntity job = findJobByJobId(theJobId); + return job.toJson(); + } + + @Transactional + @Override + public BulkImportJobFileJson fetchFile(String theJobId, int theFileIndex) { + BulkImportJobEntity job = findJobByJobId(theJobId); + + return myJobFileDao + .findForJob(job, theFileIndex) + .map(t -> t.toJson()) + .orElseThrow(() -> new IllegalArgumentException("Invalid index " + theFileIndex + " for job " + theJobId)); + } + + @Override + @Transactional + public void deleteJobFiles(String theJobId) { + BulkImportJobEntity job = findJobByJobId(theJobId); + List files = myJobFileDao.findAllIdsForJob(theJobId); + for (Long next : files) { + myJobFileDao.deleteById(next); + } + myJobDao.delete(job); + } + + private void processJob(BulkImportJobEntity theBulkExportJobEntity) throws JobParametersInvalidException { + String jobId = theBulkExportJobEntity.getJobId(); + int batchSize = theBulkExportJobEntity.getBatchSize(); + ValidateUtil.isTrueOrThrowInvalidRequest(batchSize > 0, "Batch size must be positive"); + + JobParametersBuilder parameters = new JobParametersBuilder() + .addString(BulkExportJobConfig.JOB_UUID_PARAMETER, jobId) + .addLong(BulkImportJobConfig.JOB_PARAM_COMMIT_INTERVAL, (long) batchSize); + + if(isNotBlank(theBulkExportJobEntity.getJobDescription())) { + parameters.addString(BulkExportJobConfig.JOB_DESCRIPTION, theBulkExportJobEntity.getJobDescription()); + } + + ourLog.info("Submitting bulk import job {} to job scheduler", jobId); + + myJobSubmitter.runJob(myBulkImportJob, parameters.toJobParameters()); + } + + private void addFilesToJob(@Nonnull List theInitialFiles, BulkImportJobEntity job, int nextSequence) { + for (BulkImportJobFileJson nextFile : theInitialFiles) { + ValidateUtil.isNotBlankOrThrowUnprocessableEntity(nextFile.getContents(), "Job File Contents mode must not be null"); + + BulkImportJobFileEntity jobFile = new BulkImportJobFileEntity(); + jobFile.setJob(job); + jobFile.setContents(nextFile.getContents()); + jobFile.setTenantName(nextFile.getTenantName()); + jobFile.setFileSequence(nextSequence++); + myJobFileDao.save(jobFile); + } + } + + + public static class ActivationJob implements HapiJob { + @Autowired + private IBulkDataImportSvc myTarget; + + @Override + public void execute(JobExecutionContext theContext) { + myTarget.activateNextReadyJob(); + } + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java index a625e1d8ed3..e2a5f608117 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseConfig.java @@ -11,15 +11,18 @@ import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IDao; import ca.uhn.fhir.jpa.api.model.ExpungeOptions; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; +import ca.uhn.fhir.jpa.batch.BatchConstants; import ca.uhn.fhir.jpa.batch.BatchJobsConfig; import ca.uhn.fhir.jpa.batch.api.IBatchJobSubmitter; import ca.uhn.fhir.jpa.batch.config.NonPersistedBatchConfigurer; import ca.uhn.fhir.jpa.batch.svc.BatchJobSubmitterImpl; import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider; import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; -import ca.uhn.fhir.jpa.bulk.provider.BulkDataExportProvider; -import ca.uhn.fhir.jpa.bulk.svc.BulkDataExportSvcImpl; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.provider.BulkDataExportProvider; +import ca.uhn.fhir.jpa.bulk.export.svc.BulkDataExportSvcImpl; +import ca.uhn.fhir.jpa.bulk.imprt.api.IBulkDataImportSvc; +import ca.uhn.fhir.jpa.bulk.imprt.svc.BulkDataImportSvcImpl; import ca.uhn.fhir.jpa.cache.IResourceVersionSvc; import ca.uhn.fhir.jpa.cache.ResourceVersionSvcDaoImpl; import ca.uhn.fhir.jpa.dao.DaoSearchParamProvider; @@ -29,6 +32,7 @@ import ca.uhn.fhir.jpa.dao.ISearchBuilder; import ca.uhn.fhir.jpa.dao.LegacySearchBuilder; import ca.uhn.fhir.jpa.dao.MatchResourceUrlService; import ca.uhn.fhir.jpa.dao.SearchBuilderFactory; +import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.dao.expunge.DeleteExpungeService; import ca.uhn.fhir.jpa.dao.expunge.ExpungeEverythingService; import ca.uhn.fhir.jpa.dao.expunge.ExpungeOperation; @@ -63,7 +67,6 @@ import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor; import ca.uhn.fhir.jpa.interceptor.JpaConsentContextServices; import ca.uhn.fhir.jpa.interceptor.MdmSearchExpandingInterceptor; import ca.uhn.fhir.jpa.interceptor.OverridePathBasedReferentialIntegrityForDeletesInterceptor; -import ca.uhn.fhir.rest.server.interceptor.ResponseTerminologyTranslationInterceptor; import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder; import ca.uhn.fhir.jpa.model.sched.ISchedulerService; import ca.uhn.fhir.jpa.packages.IHapiPackageCacheManager; @@ -95,8 +98,8 @@ import ca.uhn.fhir.jpa.search.builder.predicate.CoordsPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.DatePredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ForcedIdPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.NumberPredicateBuilder; -import ca.uhn.fhir.jpa.search.builder.predicate.QuantityPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.QuantityNormalizedPredicateBuilder; +import ca.uhn.fhir.jpa.search.builder.predicate.QuantityPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceIdPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceLinkPredicateBuilder; import ca.uhn.fhir.jpa.search.builder.predicate.ResourceTablePredicateBuilder; @@ -129,6 +132,7 @@ import ca.uhn.fhir.jpa.util.MemoryCacheService; import ca.uhn.fhir.jpa.validation.JpaResourceLoader; import ca.uhn.fhir.jpa.validation.ValidationSettings; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.interceptor.ResponseTerminologyTranslationInterceptor; import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices; import ca.uhn.fhir.rest.server.interceptor.partition.RequestTenantPartitionInterceptor; import org.hibernate.jpa.HibernatePersistenceProvider; @@ -160,6 +164,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import java.util.Date; +import java.util.concurrent.RejectedExecutionHandler; /* * #%L @@ -185,7 +190,7 @@ import java.util.Date; @Configuration @EnableJpaRepositories(basePackages = "ca.uhn.fhir.jpa.dao.data") @Import({ - SearchParamConfig.class, BatchJobsConfig.class + SearchParamConfig.class, BatchJobsConfig.class }) @EnableBatchProcessing public abstract class BaseConfig { @@ -199,24 +204,23 @@ public abstract class BaseConfig { public static final String PERSISTED_JPA_SEARCH_FIRST_PAGE_BUNDLE_PROVIDER = "PersistedJpaSearchFirstPageBundleProvider"; public static final String SEARCH_BUILDER = "SearchBuilder"; public static final String HISTORY_BUILDER = "HistoryBuilder"; - private static final String HAPI_DEFAULT_SCHEDULER_GROUP = "HAPI"; public static final String REPOSITORY_VALIDATING_RULE_BUILDER = "repositoryValidatingRuleBuilder"; - + private static final String HAPI_DEFAULT_SCHEDULER_GROUP = "HAPI"; @Autowired protected Environment myEnv; @Autowired private DaoRegistry myDaoRegistry; + private Integer searchCoordCorePoolSize = 20; + private Integer searchCoordMaxPoolSize = 100; + private Integer searchCoordQueueCapacity = 200; /** * Subclasses may override this method to provide settings such as search coordinator pool sizes. */ @PostConstruct - public void initSettings() {} - - private Integer searchCoordCorePoolSize = 20; - private Integer searchCoordMaxPoolSize = 100; - private Integer searchCoordQueueCapacity = 200; + public void initSettings() { + } public void setSearchCoordCorePoolSize(Integer searchCoordCorePoolSize) { this.searchCoordCorePoolSize = searchCoordCorePoolSize; @@ -297,6 +301,11 @@ public abstract class BaseConfig { return new SubscriptionTriggeringProvider(); } + @Bean + public TransactionProcessor transactionProcessor() { + return new TransactionProcessor(); + } + @Bean(name = "myAttachmentBinaryAccessProvider") @Lazy public BinaryAccessProvider binaryAccessProvider() { @@ -381,13 +390,15 @@ public abstract class BaseConfig { return retVal; } - @Bean + @Bean(name= BatchConstants.JOB_LAUNCHING_TASK_EXECUTOR) public TaskExecutor jobLaunchingTaskExecutor() { ThreadPoolTaskExecutor asyncTaskExecutor = new ThreadPoolTaskExecutor(); - asyncTaskExecutor.setCorePoolSize(5); + asyncTaskExecutor.setCorePoolSize(0); asyncTaskExecutor.setMaxPoolSize(10); - asyncTaskExecutor.setQueueCapacity(500); + asyncTaskExecutor.setQueueCapacity(0); + asyncTaskExecutor.setAllowCoreThreadTimeOut(true); asyncTaskExecutor.setThreadNamePrefix("JobLauncher-"); + asyncTaskExecutor.setRejectedExecutionHandler(new ResourceReindexingSvcImpl.BlockPolicy()); asyncTaskExecutor.initialize(); return asyncTaskExecutor; } @@ -514,6 +525,11 @@ public abstract class BaseConfig { return new BulkDataExportProvider(); } + @Bean + @Lazy + public IBulkDataImportSvc bulkDataImportSvc() { + return new BulkDataImportSvcImpl(); + } @Bean public PersistedJpaBundleProviderFactory persistedJpaBundleProviderFactory() { @@ -614,7 +630,7 @@ public abstract class BaseConfig { public QuantityNormalizedPredicateBuilder newQuantityNormalizedPredicateBuilder(SearchQueryBuilder theSearchBuilder) { return new QuantityNormalizedPredicateBuilder(theSearchBuilder); } - + @Bean @Scope("prototype") public ResourceLinkPredicateBuilder newResourceLinkPredicateBuilder(QueryStack theQueryStack, SearchQueryBuilder theSearchBuilder, boolean theReversed) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java index c012ebbfc94..aebba0133b4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/BaseDstu2Config.java @@ -7,6 +7,9 @@ import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.dao.FulltextSearchSvcImpl; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.dao.JpaPersistedResourceValidationSupport; +import ca.uhn.fhir.jpa.dao.TransactionProcessor; +import ca.uhn.fhir.jpa.dao.TransactionProcessorVersionAdapterDstu2; +import ca.uhn.fhir.jpa.dao.r4.TransactionProcessorVersionAdapterR4; import ca.uhn.fhir.jpa.term.TermReadSvcDstu2; import ca.uhn.fhir.jpa.term.api.ITermReadSvc; import ca.uhn.fhir.jpa.util.ResourceCountCache; @@ -93,6 +96,12 @@ public class BaseDstu2Config extends BaseConfig { return retVal; } + @Bean + public TransactionProcessor.ITransactionProcessorVersionAdapter transactionProcessorVersionFacade() { + return new TransactionProcessorVersionAdapterDstu2(); + } + + @Bean(name = "myDefaultProfileValidationSupport") public DefaultProfileValidationSupport defaultProfileValidationSupport() { return new DefaultProfileValidationSupport(fhirContext()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java index aa9472737c8..92529b25f5b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/dstu3/BaseDstu3Config.java @@ -87,11 +87,6 @@ public class BaseDstu3Config extends BaseConfigDstu3Plus { return new TransactionProcessorVersionAdapterDstu3(); } - @Bean - public TransactionProcessor transactionProcessor() { - return new TransactionProcessor(); - } - @Bean(name = "myResourceCountsCache") public ResourceCountCache resourceCountsCache() { ResourceCountCache retVal = new ResourceCountCache(() -> systemDaoDstu3().getResourceCounts()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java index beb5bc4eea0..6879d7dfd1b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r4/BaseR4Config.java @@ -82,11 +82,6 @@ public class BaseR4Config extends BaseConfigDstu3Plus { return new TransactionProcessorVersionAdapterR4(); } - @Bean - public TransactionProcessor transactionProcessor() { - return new TransactionProcessor(); - } - @Bean(name = GRAPHQL_PROVIDER_NAME) @Lazy public GraphQLProvider graphQLProvider() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r5/BaseR5Config.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r5/BaseR5Config.java index 6ccb22fef95..c217a864907 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r5/BaseR5Config.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/r5/BaseR5Config.java @@ -80,11 +80,6 @@ public class BaseR5Config extends BaseConfigDstu3Plus { return new TransactionProcessorVersionAdapterR5(); } - @Bean - public TransactionProcessor transactionProcessor() { - return new TransactionProcessor(); - } - @Bean(name = GRAPHQL_PROVIDER_NAME) @Lazy public GraphQLProvider graphQLProvider() { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java index da680a8f017..7e1877cc77d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirSystemDao.java @@ -3,13 +3,14 @@ package ca.uhn.fhir.jpa.dao; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.api.model.ExpungeOptions; import ca.uhn.fhir.jpa.api.model.ExpungeOutcome; -import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.util.ResourceCountCache; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import ca.uhn.fhir.util.StopWatch; +import com.google.common.annotations.VisibleForTesting; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -17,6 +18,7 @@ import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Nullable; +import javax.annotation.PostConstruct; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -42,14 +44,26 @@ import java.util.Map; * #L% */ -public abstract class BaseHapiFhirSystemDao extends BaseHapiFhirDao implements IFhirSystemDao { +public abstract class BaseHapiFhirSystemDao extends BaseHapiFhirDao implements IFhirSystemDao { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseHapiFhirSystemDao.class); @Autowired @Qualifier("myResourceCountsCache") public ResourceCountCache myResourceCountsCache; @Autowired - private PartitionSettings myPartitionSettings; + private TransactionProcessor myTransactionProcessor; + + @VisibleForTesting + public void setTransactionProcessorForUnitTest(TransactionProcessor theTransactionProcessor) { + myTransactionProcessor = theTransactionProcessor; + } + + @Override + @PostConstruct + public void start() { + super.start(); + myTransactionProcessor.setDao(this); + } @Override @Transactional(propagation = Propagation.NEVER) @@ -91,6 +105,18 @@ public abstract class BaseHapiFhirSystemDao extends BaseHapiFhirDao BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest) { + public BUNDLE transaction(RequestDetails theRequestDetails, BUNDLE theRequest, boolean theNestedMode) { if (theRequestDetails != null && theRequestDetails.getServer() != null && myDao != null) { IServerInterceptor.ActionRequestDetails requestDetails = new IServerInterceptor.ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null); myDao.notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails); } String actionName = "Transaction"; - IBaseBundle response = processTransactionAsSubRequest((RequestDetails) theRequestDetails, theRequest, actionName); + IBaseBundle response = processTransactionAsSubRequest(theRequestDetails, theRequest, actionName, theNestedMode); List entries = myVersionAdapter.getEntries(response); for (int i = 0; i < entries.size(); i++) { @@ -190,7 +191,7 @@ public abstract class BaseTransactionProcessor { myVersionAdapter.setRequestUrl(entry, next.getIdElement().toUnqualifiedVersionless().getValue()); } - transaction(theRequestDetails, transactionBundle); + transaction(theRequestDetails, transactionBundle, false); return resp; } @@ -270,10 +271,10 @@ public abstract class BaseTransactionProcessor { myDao = theDao; } - private IBaseBundle processTransactionAsSubRequest(RequestDetails theRequestDetails, IBaseBundle theRequest, String theActionName) { + private IBaseBundle processTransactionAsSubRequest(RequestDetails theRequestDetails, IBaseBundle theRequest, String theActionName, boolean theNestedMode) { BaseHapiFhirDao.markRequestAsProcessingSubRequest(theRequestDetails); try { - return processTransaction(theRequestDetails, theRequest, theActionName); + return processTransaction(theRequestDetails, theRequest, theActionName, theNestedMode); } finally { BaseHapiFhirDao.clearRequestAsProcessingSubRequest(theRequestDetails); } @@ -289,7 +290,7 @@ public abstract class BaseTransactionProcessor { myTxManager = theTxManager; } - private IBaseBundle batch(final RequestDetails theRequestDetails, IBaseBundle theRequest) { + private IBaseBundle batch(final RequestDetails theRequestDetails, IBaseBundle theRequest, boolean theNestedMode) { ourLog.info("Beginning batch with {} resources", myVersionAdapter.getEntries(theRequest).size()); long start = System.currentTimeMillis(); @@ -310,7 +311,7 @@ public abstract class BaseTransactionProcessor { IBaseBundle subRequestBundle = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.TRANSACTION.toCode()); myVersionAdapter.addEntry(subRequestBundle, (IBase) nextRequestEntry); - IBaseBundle nextResponseBundle = processTransactionAsSubRequest((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request"); + IBaseBundle nextResponseBundle = processTransactionAsSubRequest(theRequestDetails, subRequestBundle, "Batch sub-request", theNestedMode); IBase subResponseEntry = (IBase) myVersionAdapter.getEntries(nextResponseBundle).get(0); myVersionAdapter.addEntry(resp, subResponseEntry); @@ -341,7 +342,7 @@ public abstract class BaseTransactionProcessor { } long delay = System.currentTimeMillis() - start; - ourLog.info("Batch completed in {}ms", new Object[]{delay}); + ourLog.info("Batch completed in {}ms", delay); return resp; } @@ -351,13 +352,13 @@ public abstract class BaseTransactionProcessor { myHapiTransactionService = theHapiTransactionService; } - private IBaseBundle processTransaction(final RequestDetails theRequestDetails, final IBaseBundle theRequest, final String theActionName) { + private IBaseBundle processTransaction(final RequestDetails theRequestDetails, final IBaseBundle theRequest, final String theActionName, boolean theNestedMode) { validateDependencies(); String transactionType = myVersionAdapter.getBundleType(theRequest); if (org.hl7.fhir.r4.model.Bundle.BundleType.BATCH.toCode().equals(transactionType)) { - return batch(theRequestDetails, theRequest); + return batch(theRequestDetails, theRequest, theNestedMode); } if (transactionType == null) { @@ -465,6 +466,10 @@ public abstract class BaseTransactionProcessor { } for (IBase nextReqEntry : getEntries) { + if (theNestedMode) { + throw new InvalidRequestException("Can not invoke read operation on nested transaction"); + } + if (!(theRequestDetails instanceof ServletRequestDetails)) { throw new MethodNotAllowedException("Can not call transaction GET methods from this context"); } @@ -976,7 +981,12 @@ public abstract class BaseTransactionProcessor { } } - IPrimitiveType deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) nextResource); + IPrimitiveType deletedInstantOrNull; + if (nextResource instanceof IAnyResource) { + deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IAnyResource) nextResource); + } else { + deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get((IResource) nextResource); + } Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; IFhirResourceDao dao = myDaoRegistry.getResourceDao(nextResource.getClass()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java index 9581599cac3..81ab30c20b7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirSystemDaoDstu2.java @@ -20,559 +20,19 @@ package ca.uhn.fhir.jpa.dao; * #L% */ -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.jpa.api.dao.DaoRegistry; -import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; -import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; -import ca.uhn.fhir.jpa.api.model.DeleteConflictList; -import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome; -import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService; -import ca.uhn.fhir.jpa.delete.DeleteConflictService; -import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource; -import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId; -import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.entity.TagDefinition; -import ca.uhn.fhir.rest.api.server.storage.TransactionDetails; -import ca.uhn.fhir.jpa.searchparam.MatchUrlService; -import ca.uhn.fhir.model.api.IResource; -import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; -import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt; import ca.uhn.fhir.model.dstu2.composite.MetaDt; import ca.uhn.fhir.model.dstu2.resource.Bundle; -import ca.uhn.fhir.model.dstu2.resource.Bundle.Entry; -import ca.uhn.fhir.model.dstu2.resource.Bundle.EntryResponse; -import ca.uhn.fhir.model.dstu2.resource.OperationOutcome; -import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum; -import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum; -import ca.uhn.fhir.model.dstu2.valueset.IssueSeverityEnum; -import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.model.primitive.InstantDt; -import ca.uhn.fhir.model.primitive.UriDt; -import ca.uhn.fhir.parser.DataFormatException; -import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.RestfulServerUtils; -import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; -import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; -import ca.uhn.fhir.rest.server.method.BaseMethodBinding; -import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import ca.uhn.fhir.rest.server.servlet.ServletSubRequestDetails; -import ca.uhn.fhir.util.FhirTerser; -import ca.uhn.fhir.util.UrlUtil; -import ca.uhn.fhir.util.UrlUtil.UrlParts; -import com.google.common.collect.ArrayListMultimap; -import org.apache.http.NameValuePair; import org.hl7.fhir.instance.model.api.IBaseBundle; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionCallback; -import org.springframework.transaction.support.TransactionTemplate; import javax.persistence.TypedQuery; -import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; -import java.util.Set; - -import static org.apache.commons.lang3.StringUtils.defaultString; -import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.apache.commons.lang3.StringUtils.isNotBlank; public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoDstu2.class); - - @Autowired - private PlatformTransactionManager myTxManager; - @Autowired - private MatchUrlService myMatchUrlService; - @Autowired - private DaoRegistry myDaoRegistry; - @Autowired - private MatchResourceUrlService myMatchResourceUrlService; - @Autowired - private HapiTransactionService myHapiTransactionalService; - - private Bundle batch(final RequestDetails theRequestDetails, Bundle theRequest) { - ourLog.info("Beginning batch with {} resources", theRequest.getEntry().size()); - long start = System.currentTimeMillis(); - - Bundle resp = new Bundle(); - resp.setType(BundleTypeEnum.BATCH_RESPONSE); - - /* - * For batch, we handle each entry as a mini-transaction in its own database transaction so that if one fails, it doesn't prevent others - */ - - for (final Entry nextRequestEntry : theRequest.getEntry()) { - - TransactionCallback callback = new TransactionCallback() { - @Override - public Bundle doInTransaction(TransactionStatus theStatus) { - Bundle subRequestBundle = new Bundle(); - subRequestBundle.setType(BundleTypeEnum.TRANSACTION); - subRequestBundle.addEntry(nextRequestEntry); - return transaction((ServletRequestDetails) theRequestDetails, subRequestBundle, "Batch sub-request"); - } - }; - - BaseServerResponseException caughtEx; - try { - Bundle nextResponseBundle; - if (nextRequestEntry.getRequest().getMethodElement().getValueAsEnum() == HTTPVerbEnum.GET) { - // Don't process GETs in a transaction because they'll - // create their own - nextResponseBundle = callback.doInTransaction(null); - } else { - nextResponseBundle = myHapiTransactionalService.execute(theRequestDetails, callback); - } - caughtEx = null; - - Entry subResponseEntry = nextResponseBundle.getEntry().get(0); - resp.addEntry(subResponseEntry); - /* - * If the individual entry didn't have a resource in its response, bring the sub-transaction's OperationOutcome across so the client can see it - */ - if (subResponseEntry.getResource() == null) { - subResponseEntry.setResource(nextResponseBundle.getEntry().get(0).getResource()); - } - - } catch (BaseServerResponseException e) { - caughtEx = e; - } catch (Throwable t) { - ourLog.error("Failure during BATCH sub transaction processing", t); - caughtEx = new InternalErrorException(t); - } - - if (caughtEx != null) { - Entry nextEntry = resp.addEntry(); - - OperationOutcome oo = new OperationOutcome(); - oo.addIssue().setSeverity(IssueSeverityEnum.ERROR).setDiagnostics(caughtEx.getMessage()); - nextEntry.setResource(oo); - - EntryResponse nextEntryResp = nextEntry.getResponse(); - nextEntryResp.setStatus(toStatusString(caughtEx.getStatusCode())); - } - - } - - long delay = System.currentTimeMillis() - start; - ourLog.info("Batch completed in {}ms", new Object[] {delay}); - - return resp; - } - - @SuppressWarnings("unchecked") - private Bundle doTransaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) { - BundleTypeEnum transactionType = theRequest.getTypeElement().getValueAsEnum(); - if (transactionType == BundleTypeEnum.BATCH) { - return batch(theRequestDetails, theRequest); - } - - return doTransaction(theRequestDetails, theRequest, theActionName, transactionType); - } - - private Bundle doTransaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName, BundleTypeEnum theTransactionType) { - if (theTransactionType == null) { - String message = "Transaction Bundle did not specify valid Bundle.type, assuming " + BundleTypeEnum.TRANSACTION.getCode(); - ourLog.warn(message); - theTransactionType = BundleTypeEnum.TRANSACTION; - } - if (theTransactionType != BundleTypeEnum.TRANSACTION) { - throw new InvalidRequestException("Unable to process transaction where incoming Bundle.type = " + theTransactionType.getCode()); - } - - ourLog.info("Beginning {} with {} resources", theActionName, theRequest.getEntry().size()); - - long start = System.currentTimeMillis(); - TransactionDetails transactionDetails = new TransactionDetails(); - - Set allIds = new LinkedHashSet(); - Map idSubstitutions = new HashMap(); - Map idToPersistedOutcome = new HashMap(); - - /* - * We want to execute the transaction request bundle elements in the order - * specified by the FHIR specification (see TransactionSorter) so we save the - * original order in the request, then sort it. - * - * Entries with a type of GET are removed from the bundle so that they - * can be processed at the very end. We do this because the incoming resources - * are saved in a two-phase way in order to deal with interdependencies, and - * we want the GET processing to use the final indexing state - */ - Bundle response = new Bundle(); - List getEntries = new ArrayList(); - IdentityHashMap originalRequestOrder = new IdentityHashMap(); - for (int i = 0; i < theRequest.getEntry().size(); i++) { - originalRequestOrder.put(theRequest.getEntry().get(i), i); - response.addEntry(); - if (theRequest.getEntry().get(i).getRequest().getMethodElement().getValueAsEnum() == HTTPVerbEnum.GET) { - getEntries.add(theRequest.getEntry().get(i)); - } - } - Collections.sort(theRequest.getEntry(), new TransactionSorter()); - - List deletedResources = new ArrayList<>(); - DeleteConflictList deleteConflicts = new DeleteConflictList(); - Map entriesToProcess = new IdentityHashMap<>(); - Set nonUpdatedEntities = new HashSet<>(); - Set updatedEntities = new HashSet<>(); - - /* - * Handle: GET/PUT/POST - */ - TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); - txTemplate.execute(t->{ - handleTransactionWriteOperations(theRequestDetails, theRequest, theActionName, transactionDetails, allIds, idSubstitutions, idToPersistedOutcome, response, originalRequestOrder, deletedResources, deleteConflicts, entriesToProcess, nonUpdatedEntities, updatedEntities); - return null; - }); - - /* - * Loop through the request and process any entries of type GET - */ - for (int i = 0; i < getEntries.size(); i++) { - Entry nextReqEntry = getEntries.get(i); - Integer originalOrder = originalRequestOrder.get(nextReqEntry); - Entry nextRespEntry = response.getEntry().get(originalOrder); - - ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(theRequestDetails); - requestDetails.setServletRequest(theRequestDetails.getServletRequest()); - requestDetails.setRequestType(RequestTypeEnum.GET); - requestDetails.setServer(theRequestDetails.getServer()); - - String url = extractTransactionUrlOrThrowException(nextReqEntry, HTTPVerbEnum.GET); - - int qIndex = url.indexOf('?'); - ArrayListMultimap paramValues = ArrayListMultimap.create(); - requestDetails.setParameters(new HashMap()); - if (qIndex != -1) { - String params = url.substring(qIndex); - List parameters = UrlUtil.translateMatchUrl(params); - for (NameValuePair next : parameters) { - paramValues.put(next.getName(), next.getValue()); - } - for (Map.Entry> nextParamEntry : paramValues.asMap().entrySet()) { - String[] nextValue = nextParamEntry.getValue().toArray(new String[nextParamEntry.getValue().size()]); - requestDetails.addParameter(nextParamEntry.getKey(), nextValue); - } - url = url.substring(0, qIndex); - } - - requestDetails.setRequestPath(url); - requestDetails.setFhirServerBase(theRequestDetails.getFhirServerBase()); - - theRequestDetails.getServer().populateRequestDetailsFromRequestPath(requestDetails, url); - BaseMethodBinding method = theRequestDetails.getServer().determineResourceMethod(requestDetails, url); - if (method == null) { - throw new IllegalArgumentException("Unable to handle GET " + url); - } - - if (isNotBlank(nextReqEntry.getRequest().getIfMatch())) { - requestDetails.addHeader(Constants.HEADER_IF_MATCH, nextReqEntry.getRequest().getIfMatch()); - } - if (isNotBlank(nextReqEntry.getRequest().getIfNoneExist())) { - requestDetails.addHeader(Constants.HEADER_IF_NONE_EXIST, nextReqEntry.getRequest().getIfNoneExist()); - } - if (isNotBlank(nextReqEntry.getRequest().getIfNoneMatch())) { - requestDetails.addHeader(Constants.HEADER_IF_NONE_MATCH, nextReqEntry.getRequest().getIfNoneMatch()); - } - - if (method instanceof BaseResourceReturningMethodBinding) { - try { - IBaseResource resource = ((BaseResourceReturningMethodBinding) method).doInvokeServer(theRequestDetails.getServer(), requestDetails); - if (paramValues.containsKey(Constants.PARAM_SUMMARY) || paramValues.containsKey(Constants.PARAM_CONTENT)) { - resource = filterNestedBundle(requestDetails, resource); - } - nextRespEntry.setResource((IResource) resource); - nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK)); - } catch (NotModifiedException e) { - nextRespEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_304_NOT_MODIFIED)); - } - } else { - throw new IllegalArgumentException("Unable to handle GET " + url); - } - - } - - for (Map.Entry nextEntry : entriesToProcess.entrySet()) { - nextEntry.getKey().getResponse().setLocation(nextEntry.getValue().getIdDt().toUnqualified().getValue()); - nextEntry.getKey().getResponse().setEtag(nextEntry.getValue().getIdDt().getVersionIdPart()); - } - - long delay = System.currentTimeMillis() - start; - int numEntries = theRequest.getEntry().size(); - long delayPer = delay / numEntries; - ourLog.info("{} completed in {}ms ({} entries at {}ms per entry)", theActionName, delay, numEntries, delayPer); - - response.setType(BundleTypeEnum.TRANSACTION_RESPONSE); - return response; - } - - private void handleTransactionWriteOperations(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName, TransactionDetails theTransactionDetails, Set theAllIds, Map theIdSubstitutions, Map theIdToPersistedOutcome, Bundle theResponse, IdentityHashMap theOriginalRequestOrder, List theDeletedResources, DeleteConflictList theDeleteConflicts, Map theEntriesToProcess, Set theNonUpdatedEntities, Set theUpdatedEntities) { - /* - * Loop through the request and process any entries of type - * PUT, POST or DELETE - */ - for (int i = 0; i < theRequest.getEntry().size(); i++) { - - if (i % 100 == 0) { - ourLog.debug("Processed {} non-GET entries out of {}", i, theRequest.getEntry().size()); - } - - Entry nextReqEntry = theRequest.getEntry().get(i); - IResource res = nextReqEntry.getResource(); - IdDt nextResourceId = null; - if (res != null) { - - nextResourceId = res.getId(); - - if (!nextResourceId.hasIdPart()) { - if (isNotBlank(nextReqEntry.getFullUrl())) { - nextResourceId = new IdDt(nextReqEntry.getFullUrl()); - } - } - - if (nextResourceId.hasIdPart() && nextResourceId.getIdPart().matches("[a-zA-Z]+:.*") && !isPlaceholder(nextResourceId)) { - throw new InvalidRequestException("Invalid placeholder ID found: " + nextResourceId.getIdPart() + " - Must be of the form 'urn:uuid:[uuid]' or 'urn:oid:[oid]'"); - } - - if (nextResourceId.hasIdPart() && !nextResourceId.hasResourceType() && !isPlaceholder(nextResourceId)) { - nextResourceId = new IdDt(toResourceName(res.getClass()), nextResourceId.getIdPart()); - res.setId(nextResourceId); - } - - /* - * Ensure that the bundle doesn't have any duplicates, since this causes all kinds of weirdness - */ - if (isPlaceholder(nextResourceId)) { - if (!theAllIds.add(nextResourceId)) { - throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextResourceId)); - } - } else if (nextResourceId.hasResourceType() && nextResourceId.hasIdPart()) { - IdDt nextId = nextResourceId.toUnqualifiedVersionless(); - if (!theAllIds.add(nextId)) { - throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionContainsMultipleWithDuplicateId", nextId)); - } - } - - } - - HTTPVerbEnum verb = nextReqEntry.getRequest().getMethodElement().getValueAsEnum(); - if (verb == null) { - throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionEntryHasInvalidVerb", nextReqEntry.getRequest().getMethod())); - } - - String resourceType = res != null ? getContext().getResourceType(res) : null; - Entry nextRespEntry = theResponse.getEntry().get(theOriginalRequestOrder.get(nextReqEntry)); - - switch (verb) { - case POST: { - // CREATE - @SuppressWarnings("rawtypes") - IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(res.getClass()); - res.setId((String) null); - DaoMethodOutcome outcome; - outcome = resourceDao.create(res, nextReqEntry.getRequest().getIfNoneExist(), false, theTransactionDetails, theRequestDetails); - handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res); - theEntriesToProcess.put(nextRespEntry, outcome.getEntity()); - if (outcome.getCreated() == false) { - theNonUpdatedEntities.add(outcome.getEntity()); - } - break; - } - case DELETE: { - // DELETE - String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); - UrlParts parts = UrlUtil.parseUrl(url); - IFhirResourceDao dao = toDao(parts, verb.getCode(), url); - int status = Constants.STATUS_HTTP_204_NO_CONTENT; - if (parts.getResourceId() != null) { - DaoMethodOutcome outcome = dao.delete(new IdDt(parts.getResourceType(), parts.getResourceId()), theDeleteConflicts, theRequestDetails, theTransactionDetails); - if (outcome.getEntity() != null) { - theDeletedResources.add(outcome.getId().toUnqualifiedVersionless()); - theEntriesToProcess.put(nextRespEntry, outcome.getEntity()); - } - } else { - DeleteMethodOutcome deleteOutcome = dao.deleteByUrl(parts.getResourceType() + '?' + parts.getParams(), theDeleteConflicts, theRequestDetails); - List allDeleted = deleteOutcome.getDeletedEntities(); - for (ResourceTable deleted : allDeleted) { - theDeletedResources.add(deleted.getIdDt().toUnqualifiedVersionless()); - } - if (allDeleted.isEmpty()) { - status = Constants.STATUS_HTTP_404_NOT_FOUND; - } - } - - nextRespEntry.getResponse().setStatus(toStatusString(status)); - break; - } - case PUT: { - // UPDATE - @SuppressWarnings("rawtypes") - IFhirResourceDao resourceDao = myDaoRegistry.getResourceDao(res.getClass()); - - DaoMethodOutcome outcome; - - String url = extractTransactionUrlOrThrowException(nextReqEntry, verb); - - UrlParts parts = UrlUtil.parseUrl(url); - if (isNotBlank(parts.getResourceId())) { - res.setId(new IdDt(parts.getResourceType(), parts.getResourceId())); - outcome = resourceDao.update(res, null, false, theRequestDetails); - } else { - res.setId((String) null); - outcome = resourceDao.update(res, parts.getResourceType() + '?' + parts.getParams(), false, theRequestDetails); - } - - if (outcome.getCreated() == Boolean.FALSE) { - theUpdatedEntities.add(outcome.getEntity()); - } - - handleTransactionCreateOrUpdateOutcome(theIdSubstitutions, theIdToPersistedOutcome, nextResourceId, outcome, nextRespEntry, resourceType, res); - theEntriesToProcess.put(nextRespEntry, outcome.getEntity()); - break; - } - case GET: - break; - } - } - - /* - * Make sure that there are no conflicts from deletions. E.g. we can't delete something - * if something else has a reference to it.. Unless the thing that has a reference to it - * was also deleted as a part of this transaction, which is why we check this now at the - * end. - */ - - theDeleteConflicts.removeIf(next -> theDeletedResources.contains(next.getTargetId().toVersionless())); - DeleteConflictService.validateDeleteConflictsEmptyOrThrowException(getContext(), theDeleteConflicts); - - /* - * Perform ID substitutions and then index each resource we have saved - */ - - FhirTerser terser = getContext().newTerser(); - for (DaoMethodOutcome nextOutcome : theIdToPersistedOutcome.values()) { - IResource nextResource = (IResource) nextOutcome.getResource(); - if (nextResource == null) { - continue; - } - - // References - List allRefs = terser.getAllPopulatedChildElementsOfType(nextResource, BaseResourceReferenceDt.class); - for (BaseResourceReferenceDt nextRef : allRefs) { - IdDt nextId = nextRef.getReference(); - if (!nextId.hasIdPart()) { - continue; - } - if (theIdSubstitutions.containsKey(nextId)) { - IdDt newId = theIdSubstitutions.get(nextId); - ourLog.debug(" * Replacing resource ref {} with {}", nextId, newId); - nextRef.setReference(newId); - } else { - ourLog.debug(" * Reference [{}] does not exist in bundle", nextId); - } - } - - // URIs - List allUris = terser.getAllPopulatedChildElementsOfType(nextResource, UriDt.class); - for (UriDt nextRef : allUris) { - if (nextRef instanceof IIdType) { - continue; // No substitution on the resource ID itself! - } - IdDt nextUriString = new IdDt(nextRef.getValueAsString()); - if (theIdSubstitutions.containsKey(nextUriString)) { - IdDt newId = theIdSubstitutions.get(nextUriString); - ourLog.debug(" * Replacing resource ref {} with {}", nextUriString, newId); - nextRef.setValue(newId.getValue()); - } else { - ourLog.debug(" * Reference [{}] does not exist in bundle", nextUriString); - } - } - - - InstantDt deletedInstantOrNull = ResourceMetadataKeyEnum.DELETED_AT.get(nextResource); - Date deletedTimestampOrNull = deletedInstantOrNull != null ? deletedInstantOrNull.getValue() : null; - if (theUpdatedEntities.contains(nextOutcome.getEntity())) { - updateInternal(theRequestDetails, nextResource, true, false, nextOutcome.getEntity(), nextResource.getIdElement(), nextOutcome.getPreviousResource(), theTransactionDetails); - } else if (!theNonUpdatedEntities.contains(nextOutcome.getEntity())) { - updateEntity(theRequestDetails, nextResource, nextOutcome.getEntity(), deletedTimestampOrNull, true, false, theTransactionDetails, false, true); - } - } - - myEntityManager.flush(); - - /* - * Double check we didn't allow any duplicates we shouldn't have - */ - for (Entry nextEntry : theRequest.getEntry()) { - if (nextEntry.getRequest().getMethodElement().getValueAsEnum() == HTTPVerbEnum.POST) { - String matchUrl = nextEntry.getRequest().getIfNoneExist(); - if (isNotBlank(matchUrl)) { - Class resType = nextEntry.getResource().getClass(); - Set val = myMatchResourceUrlService.processMatchUrl(matchUrl, resType, theRequestDetails); - if (val.size() > 1) { - throw new InvalidRequestException( - "Unable to process " + theActionName + " - Request would cause multiple resources to match URL: \"" + matchUrl + "\". Does transaction request contain duplicates?"); - } - } - } - } - - for (IdDt next : theAllIds) { - IdDt replacement = theIdSubstitutions.get(next); - if (replacement == null) { - continue; - } - if (replacement.equals(next)) { - continue; - } - ourLog.debug("Placeholder resource ID \"{}\" was replaced with permanent ID \"{}\"", next, replacement); - } - } - - private String extractTransactionUrlOrThrowException(Entry nextEntry, HTTPVerbEnum verb) { - String url = nextEntry.getRequest().getUrl(); - if (isBlank(url)) { - throw new InvalidRequestException(getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionMissingUrl", verb.name())); - } - return url; - } - - /** - * This method is called for nested bundles (e.g. if we received a transaction with an entry that - * was a GET search, this method is called on the bundle for the search result, that will be placed in the - * outer bundle). This method applies the _summary and _content parameters to the output of - * that bundle. - *

- * TODO: This isn't the most efficient way of doing this.. hopefully we can come up with something better in the future. - */ - private IBaseResource filterNestedBundle(RequestDetails theRequestDetails, IBaseResource theResource) { - IParser p = getContext().newJsonParser(); - RestfulServerUtils.configureResponseParser(theRequestDetails, p); - return p.parseResource(theResource.getClass(), p.encodeResourceToString(theResource)); - } @Override public MetaDt metaGetOperation(RequestDetails theRequestDetails) { @@ -589,31 +49,6 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { return retVal; } - private IFhirResourceDao toDao(UrlParts theParts, String theVerb, String theUrl) { - RuntimeResourceDefinition resType; - try { - resType = getContext().getResourceDefinition(theParts.getResourceType()); - } catch (DataFormatException e) { - String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); - throw new InvalidRequestException(msg); - } - IFhirResourceDao dao = null; - if (resType != null) { - dao = this.myDaoRegistry.getResourceDaoOrNull(resType.getImplementingClass()); - } - if (dao == null) { - String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); - throw new InvalidRequestException(msg); - } - - // if (theParts.getResourceId() == null && theParts.getParams() == null) { - // String msg = getContext().getLocalizer().getMessage(BaseHapiFhirSystemDao.class, "transactionInvalidUrl", theVerb, theUrl); - // throw new InvalidRequestException(msg); - // } - - return dao; - } - protected MetaDt toMetaDt(Collection tagDefinitions) { MetaDt retVal = new MetaDt(); for (TagDefinition next : tagDefinitions) { @@ -632,105 +67,9 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao { return retVal; } - @Transactional(propagation = Propagation.NEVER) - @Override - public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) { - if (theRequestDetails != null) { - ActionRequestDetails requestDetails = new ActionRequestDetails(theRequestDetails, theRequest, "Bundle", null); - notifyInterceptors(RestOperationTypeEnum.TRANSACTION, requestDetails); - } - - String actionName = "Transaction"; - return transaction((ServletRequestDetails) theRequestDetails, theRequest, actionName); - } - - private Bundle transaction(ServletRequestDetails theRequestDetails, Bundle theRequest, String theActionName) { - markRequestAsProcessingSubRequest(theRequestDetails); - try { - return doTransaction(theRequestDetails, theRequest, theActionName); - } finally { - clearRequestAsProcessingSubRequest(theRequestDetails); - } - } - - private static void handleTransactionCreateOrUpdateOutcome(Map idSubstitutions, Map idToPersistedOutcome, IdDt nextResourceId, DaoMethodOutcome outcome, - Entry newEntry, String theResourceType, IResource theRes) { - IdDt newId = (IdDt) outcome.getId().toUnqualifiedVersionless(); - IdDt resourceId = isPlaceholder(nextResourceId) ? nextResourceId : nextResourceId.toUnqualifiedVersionless(); - if (newId.equals(resourceId) == false) { - idSubstitutions.put(resourceId, newId); - if (isPlaceholder(resourceId)) { - /* - * The correct way for substitution IDs to be is to be with no resource type, but we'll accept the qualified kind too just to be lenient. - */ - idSubstitutions.put(new IdDt(theResourceType + '/' + resourceId.getValue()), newId); - } - } - idToPersistedOutcome.put(newId, outcome); - if (outcome.getCreated().booleanValue()) { - newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_201_CREATED)); - } else { - newEntry.getResponse().setStatus(toStatusString(Constants.STATUS_HTTP_200_OK)); - } - newEntry.getResponse().setLastModified(ResourceMetadataKeyEnum.UPDATED.get(theRes)); - } - - private static boolean isPlaceholder(IdDt theId) { - if (theId.getValue() != null) { - return theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:"); - } - return false; - } - - private static String toStatusString(int theStatusCode) { - return theStatusCode + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); - } - @Override public IBaseBundle processMessage(RequestDetails theRequestDetails, IBaseBundle theMessage) { return FhirResourceDaoMessageHeaderDstu2.throwProcessMessageNotImplemented(); } - - /** - * Transaction Order, per the spec: - *

- * Process any DELETE interactions - * Process any POST interactions - * Process any PUT interactions - * Process any GET interactions - */ - public class TransactionSorter implements Comparator { - - @Override - public int compare(Entry theO1, Entry theO2) { - int o1 = toOrder(theO1); - int o2 = toOrder(theO2); - - return o1 - o2; - } - - private int toOrder(Entry theO1) { - int o1 = 0; - if (theO1.getRequest().getMethodElement().getValueAsEnum() != null) { - switch (theO1.getRequest().getMethodElement().getValueAsEnum()) { - case DELETE: - o1 = 1; - break; - case POST: - o1 = 2; - break; - case PUT: - o1 = 3; - break; - case GET: - o1 = 4; - break; - } - } - return o1; - } - - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessorVersionAdapterDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessorVersionAdapterDstu2.java new file mode 100644 index 00000000000..b1b87a079e1 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessorVersionAdapterDstu2.java @@ -0,0 +1,171 @@ +package ca.uhn.fhir.jpa.dao; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.FhirContext; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.api.TemporalPrecisionEnum; +import ca.uhn.fhir.model.dstu2.resource.Bundle; +import ca.uhn.fhir.model.dstu2.resource.OperationOutcome; +import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum; +import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum; +import ca.uhn.fhir.model.dstu2.valueset.IssueSeverityEnum; +import ca.uhn.fhir.model.dstu2.valueset.IssueTypeEnum; +import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import org.hl7.fhir.exceptions.FHIRException; +import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; +import org.hl7.fhir.instance.model.api.IBaseResource; + +import java.util.Date; +import java.util.List; + +public class TransactionProcessorVersionAdapterDstu2 implements TransactionProcessor.ITransactionProcessorVersionAdapter { + @Override + public void setResponseStatus(Bundle.Entry theBundleEntry, String theStatus) { + theBundleEntry.getResponse().setStatus(theStatus); + } + + @Override + public void setResponseLastModified(Bundle.Entry theBundleEntry, Date theLastModified) { + theBundleEntry.getResponse().setLastModified(theLastModified, TemporalPrecisionEnum.MILLI); + } + + @Override + public void setResource(Bundle.Entry theBundleEntry, IBaseResource theResource) { + theBundleEntry.setResource((IResource) theResource); + } + + @Override + public IBaseResource getResource(Bundle.Entry theBundleEntry) { + return theBundleEntry.getResource(); + } + + @Override + public String getBundleType(Bundle theRequest) { + if (theRequest.getType() == null) { + return null; + } + return theRequest.getTypeElement().getValue(); + } + + @Override + public void populateEntryWithOperationOutcome(BaseServerResponseException theCaughtEx, Bundle.Entry theEntry) { + OperationOutcome oo = new OperationOutcome(); + oo.addIssue() + .setSeverity(IssueSeverityEnum.ERROR) + .setDiagnostics(theCaughtEx.getMessage()) + .setCode(IssueTypeEnum.EXCEPTION); + theEntry.setResource(oo); + } + + @Override + public Bundle createBundle(String theBundleType) { + Bundle resp = new Bundle(); + try { + resp.setType(BundleTypeEnum.forCode(theBundleType)); + } catch (FHIRException theE) { + throw new InternalErrorException("Unknown bundle type: " + theBundleType); + } + return resp; + } + + @Override + public List getEntries(Bundle theRequest) { + return theRequest.getEntry(); + } + + @Override + public void addEntry(Bundle theBundle, Bundle.Entry theEntry) { + theBundle.addEntry(theEntry); + } + + @Override + public Bundle.Entry addEntry(Bundle theBundle) { + return theBundle.addEntry(); + } + + @Override + public String getEntryRequestVerb(FhirContext theContext, Bundle.Entry theEntry) { + String retVal = null; + HTTPVerbEnum value = theEntry.getRequest().getMethodElement().getValueAsEnum(); + if (value != null) { + retVal = value.getCode(); + } + return retVal; + } + + @Override + public String getFullUrl(Bundle.Entry theEntry) { + return theEntry.getFullUrl(); + } + + @Override + public String getEntryIfNoneExist(Bundle.Entry theEntry) { + return theEntry.getRequest().getIfNoneExist(); + } + + @Override + public String getEntryRequestUrl(Bundle.Entry theEntry) { + return theEntry.getRequest().getUrl(); + } + + @Override + public void setResponseLocation(Bundle.Entry theEntry, String theResponseLocation) { + theEntry.getResponse().setLocation(theResponseLocation); + } + + @Override + public void setResponseETag(Bundle.Entry theEntry, String theEtag) { + theEntry.getResponse().setEtag(theEtag); + } + + @Override + public String getEntryRequestIfMatch(Bundle.Entry theEntry) { + return theEntry.getRequest().getIfMatch(); + } + + @Override + public String getEntryRequestIfNoneExist(Bundle.Entry theEntry) { + return theEntry.getRequest().getIfNoneExist(); + } + + @Override + public String getEntryRequestIfNoneMatch(Bundle.Entry theEntry) { + return theEntry.getRequest().getIfNoneMatch(); + } + + @Override + public void setResponseOutcome(Bundle.Entry theEntry, IBaseOperationOutcome theOperationOutcome) { + theEntry.setResource((IResource) theOperationOutcome); + } + + @Override + public void setRequestVerb(Bundle.Entry theEntry, String theVerb) { + theEntry.getRequest().setMethod(HTTPVerbEnum.forCode(theVerb)); + } + + @Override + public void setRequestUrl(Bundle.Entry theEntry, String theUrl) { + theEntry.getRequest().setUrl(theUrl); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBulkExportJobDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBulkExportJobDao.java index 708425d5fdb..c5bf0ddf606 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBulkExportJobDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBulkExportJobDao.java @@ -1,6 +1,6 @@ package ca.uhn.fhir.jpa.dao.data; -import ca.uhn.fhir.jpa.bulk.model.BulkJobStatusEnum; +import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum; import ca.uhn.fhir.jpa.entity.BulkExportJobEntity; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; @@ -38,13 +38,13 @@ public interface IBulkExportJobDao extends JpaRepository findByJobId(@Param("jobid") String theUuid); @Query("SELECT j FROM BulkExportJobEntity j WHERE j.myStatus = :status") - Slice findByStatus(Pageable thePage, @Param("status") BulkJobStatusEnum theSubmitted); + Slice findByStatus(Pageable thePage, @Param("status") BulkExportJobStatusEnum theSubmitted); @Query("SELECT j FROM BulkExportJobEntity j WHERE j.myExpiry < :cutoff") Slice findByExpiry(Pageable thePage, @Param("cutoff") Date theCutoff); @Query("SELECT j FROM BulkExportJobEntity j WHERE j.myRequest = :request AND j.myCreated > :createdAfter AND j.myStatus <> :status ORDER BY j.myCreated DESC") - Slice findExistingJob(Pageable thePage, @Param("request") String theRequest, @Param("createdAfter") Date theCreatedAfter, @Param("status") BulkJobStatusEnum theNotStatus); + Slice findExistingJob(Pageable thePage, @Param("request") String theRequest, @Param("createdAfter") Date theCreatedAfter, @Param("status") BulkExportJobStatusEnum theNotStatus); @Modifying @Query("DELETE FROM BulkExportJobEntity t") diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBulkImportJobDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBulkImportJobDao.java new file mode 100644 index 00000000000..dccaa953eb8 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBulkImportJobDao.java @@ -0,0 +1,40 @@ +package ca.uhn.fhir.jpa.dao.data; + +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum; +import ca.uhn.fhir.jpa.entity.BulkImportJobEntity; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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% + */ + +public interface IBulkImportJobDao extends JpaRepository { + + @Query("SELECT j FROM BulkImportJobEntity j WHERE j.myJobId = :jobid") + Optional findByJobId(@Param("jobid") String theUuid); + + @Query("SELECT j FROM BulkImportJobEntity j WHERE j.myStatus = :status") + Slice findByStatus(Pageable thePage, @Param("status") BulkImportJobStatusEnum theStatus); +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBulkImportJobFileDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBulkImportJobFileDao.java new file mode 100644 index 00000000000..c53e49f95a4 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IBulkImportJobFileDao.java @@ -0,0 +1,43 @@ +package ca.uhn.fhir.jpa.dao.data; + +import ca.uhn.fhir.jpa.entity.BulkImportJobEntity; +import ca.uhn.fhir.jpa.entity.BulkImportJobFileEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +/* + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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% + */ + +public interface IBulkImportJobFileDao extends JpaRepository { + + @Query("SELECT f FROM BulkImportJobFileEntity f WHERE f.myJob.myJobId = :jobId ORDER BY f.myFileSequence ASC") + List findAllForJob(@Param("jobId") String theJobId); + + @Query("SELECT f FROM BulkImportJobFileEntity f WHERE f.myJob = :job AND f.myFileSequence = :fileIndex") + Optional findForJob(@Param("job") BulkImportJobEntity theJob, @Param("fileIndex") int theFileIndex); + + @Query("SELECT f.myId FROM BulkImportJobFileEntity f WHERE f.myJob.myJobId = :jobId ORDER BY f.myFileSequence ASC") + List findAllIdsForJob(@Param("jobId") String theJobId); + +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java index 0af11e3a082..96019ae6a21 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java @@ -22,18 +22,13 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao; import ca.uhn.fhir.jpa.dao.FhirResourceDaoMessageHeaderDstu2; -import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.model.entity.TagDefinition; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import org.hl7.fhir.dstu3.model.Bundle; -import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; import org.hl7.fhir.dstu3.model.Meta; import org.hl7.fhir.instance.model.api.IBaseBundle; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; import javax.annotation.PostConstruct; import javax.persistence.TypedQuery; @@ -42,14 +37,10 @@ import java.util.List; public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { - @Autowired - private TransactionProcessor myTransactionProcessor; - @Override @PostConstruct public void start() { super.start(); - myTransactionProcessor.setDao(this); } @Override @@ -88,12 +79,5 @@ public class FhirSystemDaoDstu3 extends BaseHapiFhirSystemDao { return FhirResourceDaoMessageHeaderDstu2.throwProcessMessageNotImplemented(); } - @Transactional(propagation = Propagation.NEVER) - @Override - public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) { - return myTransactionProcessor.transaction(theRequestDetails, theRequest); - } - - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java index 5da2372c9b6..e012ee235ad 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/expunge/ExpungeEverythingService.java @@ -23,6 +23,8 @@ package ca.uhn.fhir.jpa.dao.expunge; import ca.uhn.fhir.interceptor.api.HookParams; import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.jpa.entity.BulkImportJobEntity; +import ca.uhn.fhir.jpa.entity.BulkImportJobFileEntity; import ca.uhn.fhir.jpa.entity.PartitionEntity; import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.entity.SearchInclude; @@ -123,6 +125,8 @@ public class ExpungeEverythingService { counter.addAndGet(expungeEverythingByType(NpmPackageVersionEntity.class)); counter.addAndGet(expungeEverythingByType(NpmPackageEntity.class)); counter.addAndGet(expungeEverythingByType(SearchParamPresent.class)); + counter.addAndGet(expungeEverythingByType(BulkImportJobFileEntity.class)); + counter.addAndGet(expungeEverythingByType(BulkImportJobEntity.class)); counter.addAndGet(expungeEverythingByType(ForcedId.class)); counter.addAndGet(expungeEverythingByType(ResourceIndexedSearchParamDate.class)); counter.addAndGet(expungeEverythingByType(ResourceIndexedSearchParamNumber.class)); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4.java index 04baaca4922..a369f3d7e5f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4.java @@ -22,42 +22,20 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao; import ca.uhn.fhir.jpa.dao.FhirResourceDaoMessageHeaderDstu2; -import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.model.entity.TagDefinition; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; -import com.google.common.annotations.VisibleForTesting; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Meta; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; -import javax.annotation.PostConstruct; import javax.persistence.TypedQuery; import java.util.Collection; import java.util.List; public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao { - @Autowired - private TransactionProcessor myTransactionProcessor; - - @VisibleForTesting - public void setTransactionProcessorForUnitTest(TransactionProcessor theTransactionProcessor) { - myTransactionProcessor = theTransactionProcessor; - } - - @Override - @PostConstruct - public void start() { - super.start(); - myTransactionProcessor.setDao(this); - } - - @Override public Meta metaGetOperation(RequestDetails theRequestDetails) { // Notify interceptors @@ -95,10 +73,4 @@ public class FhirSystemDaoR4 extends BaseHapiFhirSystemDao { return retVal; } - @Transactional(propagation = Propagation.NEVER) - @Override - public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) { - return myTransactionProcessor.transaction(theRequestDetails, theRequest); - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirSystemDaoR5.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirSystemDaoR5.java index 9d13bae6d1e..919d831e4a6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirSystemDaoR5.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r5/FhirSystemDaoR5.java @@ -22,20 +22,14 @@ package ca.uhn.fhir.jpa.dao.r5; import ca.uhn.fhir.jpa.dao.BaseHapiFhirSystemDao; import ca.uhn.fhir.jpa.dao.FhirResourceDaoMessageHeaderDstu2; -import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.jpa.model.entity.TagDefinition; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.r5.model.Bundle; -import org.hl7.fhir.r5.model.Bundle.BundleEntryComponent; import org.hl7.fhir.r5.model.Meta; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; -import javax.annotation.PostConstruct; import javax.persistence.TypedQuery; import java.util.Collection; import java.util.List; @@ -44,17 +38,6 @@ public class FhirSystemDaoR5 extends BaseHapiFhirSystemDao { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirSystemDaoR5.class); - @Autowired - private TransactionProcessor myTransactionProcessor; - - @Override - @PostConstruct - public void start() { - super.start(); - myTransactionProcessor.setDao(this); - } - - @Override public Meta metaGetOperation(RequestDetails theRequestDetails) { // Notify interceptors @@ -92,10 +75,5 @@ public class FhirSystemDaoR5 extends BaseHapiFhirSystemDao { return retVal; } - @Transactional(propagation = Propagation.NEVER) - @Override - public Bundle transaction(RequestDetails theRequestDetails, Bundle theRequest) { - return myTransactionProcessor.transaction(theRequestDetails, theRequest); - } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BulkExportJobEntity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BulkExportJobEntity.java index 05f68783fa7..f2f8a092715 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BulkExportJobEntity.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BulkExportJobEntity.java @@ -20,7 +20,7 @@ package ca.uhn.fhir.jpa.entity; * #L% */ -import ca.uhn.fhir.jpa.bulk.model.BulkJobStatusEnum; +import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hl7.fhir.r5.model.InstantType; @@ -51,9 +51,9 @@ import static org.apache.commons.lang3.StringUtils.left; @Entity @Table(name = "HFJ_BLK_EXPORT_JOB", uniqueConstraints = { - @UniqueConstraint(name = "IDX_BLKEX_JOB_ID", columnNames = "JOB_ID") + @UniqueConstraint(name = "IDX_BLKEX_JOB_ID", columnNames = "JOB_ID") }, indexes = { - @Index(name = "IDX_BLKEX_EXPTIME", columnList = "EXP_TIME") + @Index(name = "IDX_BLKEX_EXPTIME", columnList = "EXP_TIME") }) public class BulkExportJobEntity implements Serializable { @@ -70,7 +70,7 @@ public class BulkExportJobEntity implements Serializable { @Enumerated(EnumType.STRING) @Column(name = "JOB_STATUS", length = 10, nullable = false) - private BulkJobStatusEnum myStatus; + private BulkExportJobStatusEnum myStatus; @Temporal(TemporalType.TIMESTAMP) @Column(name = "CREATED_TIME", nullable = false) private Date myCreated; @@ -156,11 +156,11 @@ public class BulkExportJobEntity implements Serializable { return b.toString(); } - public BulkJobStatusEnum getStatus() { + public BulkExportJobStatusEnum getStatus() { return myStatus; } - public void setStatus(BulkJobStatusEnum theStatus) { + public void setStatus(BulkExportJobStatusEnum theStatus) { if (myStatus != theStatus) { myStatusTime = new Date(); myStatus = theStatus; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BulkImportJobEntity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BulkImportJobEntity.java new file mode 100644 index 00000000000..b7de7e9cc7b --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BulkImportJobEntity.java @@ -0,0 +1,157 @@ +package ca.uhn.fhir.jpa.entity; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.bulk.imprt.model.BulkImportJobJson; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum; +import ca.uhn.fhir.jpa.bulk.imprt.model.JobFileRowProcessingModeEnum; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.persistence.UniqueConstraint; +import javax.persistence.Version; +import java.io.Serializable; +import java.util.Date; + +import static org.apache.commons.lang3.StringUtils.left; + +@Entity +@Table(name = "HFJ_BLK_IMPORT_JOB", uniqueConstraints = { + @UniqueConstraint(name = "IDX_BLKIM_JOB_ID", columnNames = "JOB_ID") +}) +public class BulkImportJobEntity implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_BLKIMJOB_PID") + @SequenceGenerator(name = "SEQ_BLKIMJOB_PID", sequenceName = "SEQ_BLKIMJOB_PID") + @Column(name = "PID") + private Long myId; + + @Column(name = "JOB_ID", length = Search.UUID_COLUMN_LENGTH, nullable = false, updatable = false) + private String myJobId; + @Column(name = "JOB_DESC", nullable = true, length = BulkExportJobEntity.STATUS_MESSAGE_LEN) + private String myJobDescription; + @Enumerated(EnumType.STRING) + @Column(name = "JOB_STATUS", length = 10, nullable = false) + private BulkImportJobStatusEnum myStatus; + @Version + @Column(name = "OPTLOCK", nullable = false) + private int myVersion; + @Column(name = "FILE_COUNT", nullable = false) + private int myFileCount; + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "STATUS_TIME", nullable = false) + private Date myStatusTime; + @Column(name = "STATUS_MESSAGE", nullable = true, length = BulkExportJobEntity.STATUS_MESSAGE_LEN) + private String myStatusMessage; + @Column(name = "ROW_PROCESSING_MODE", length = 20, nullable = false, updatable = false) + @Enumerated(EnumType.STRING) + private JobFileRowProcessingModeEnum myRowProcessingMode; + @Column(name = "BATCH_SIZE", nullable = false, updatable = false) + private int myBatchSize; + + public String getJobDescription() { + return myJobDescription; + } + + public void setJobDescription(String theJobDescription) { + myJobDescription = left(theJobDescription, BulkExportJobEntity.STATUS_MESSAGE_LEN); + } + + public JobFileRowProcessingModeEnum getRowProcessingMode() { + return myRowProcessingMode; + } + + public void setRowProcessingMode(JobFileRowProcessingModeEnum theRowProcessingMode) { + myRowProcessingMode = theRowProcessingMode; + } + + public Date getStatusTime() { + return myStatusTime; + } + + public void setStatusTime(Date theStatusTime) { + myStatusTime = theStatusTime; + } + + public int getFileCount() { + return myFileCount; + } + + public void setFileCount(int theFileCount) { + myFileCount = theFileCount; + } + + public String getJobId() { + return myJobId; + } + + public void setJobId(String theJobId) { + myJobId = theJobId; + } + + public BulkImportJobStatusEnum getStatus() { + return myStatus; + } + + /** + * Sets the status, updates the status time, and clears the status message + */ + public void setStatus(BulkImportJobStatusEnum theStatus) { + if (myStatus != theStatus) { + myStatus = theStatus; + setStatusTime(new Date()); + setStatusMessage(null); + } + } + + public String getStatusMessage() { + return myStatusMessage; + } + + public void setStatusMessage(String theStatusMessage) { + myStatusMessage = left(theStatusMessage, BulkExportJobEntity.STATUS_MESSAGE_LEN); + } + + public BulkImportJobJson toJson() { + return new BulkImportJobJson() + .setProcessingMode(getRowProcessingMode()) + .setFileCount(getFileCount()) + .setJobDescription(getJobDescription()); + } + + public int getBatchSize() { + return myBatchSize; + } + + public void setBatchSize(int theBatchSize) { + myBatchSize = theBatchSize; + } +} diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BulkImportJobFileEntity.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BulkImportJobFileEntity.java new file mode 100644 index 00000000000..b1dd778a2c8 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BulkImportJobFileEntity.java @@ -0,0 +1,104 @@ +package ca.uhn.fhir.jpa.entity; + +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2021 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.bulk.imprt.model.BulkImportJobFileJson; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.ForeignKey; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.Lob; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; + +@Entity +@Table(name = "HFJ_BLK_IMPORT_JOBFILE", indexes = { + @Index(name = "IDX_BLKIM_JOBFILE_JOBID", columnList = "JOB_PID") +}) +public class BulkImportJobFileEntity implements Serializable { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_BLKIMJOBFILE_PID") + @SequenceGenerator(name = "SEQ_BLKIMJOBFILE_PID", sequenceName = "SEQ_BLKIMJOBFILE_PID") + @Column(name = "PID") + private Long myId; + + @ManyToOne + @JoinColumn(name = "JOB_PID", referencedColumnName = "PID", nullable = false, foreignKey = @ForeignKey(name = "FK_BLKIMJOBFILE_JOB")) + private BulkImportJobEntity myJob; + + @Column(name = "FILE_SEQ", nullable = false) + private int myFileSequence; + + @Lob + @Column(name = "JOB_CONTENTS", nullable = false) + private byte[] myContents; + + @Column(name = "TENANT_NAME", nullable = true, length = PartitionEntity.MAX_NAME_LENGTH) + private String myTenantName; + + public BulkImportJobEntity getJob() { + return myJob; + } + + public void setJob(BulkImportJobEntity theJob) { + myJob = theJob; + } + + public int getFileSequence() { + return myFileSequence; + } + + public void setFileSequence(int theFileSequence) { + myFileSequence = theFileSequence; + } + + public String getContents() { + return new String(myContents, StandardCharsets.UTF_8); + } + + public void setContents(String theContents) { + myContents = theContents.getBytes(StandardCharsets.UTF_8); + } + + + public BulkImportJobFileJson toJson() { + return new BulkImportJobFileJson() + .setContents(getContents()) + .setTenantName(getTenantName()); + } + + public void setTenantName(String theTenantName) { + myTenantName = theTenantName; + } + + public String getTenantName() { + return myTenantName; + } +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BaseBatchJobR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BaseBatchJobR4Test.java new file mode 100644 index 00000000000..f3e6a6a130d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BaseBatchJobR4Test.java @@ -0,0 +1,58 @@ +package ca.uhn.fhir.jpa.bulk; + +import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobInstance; +import org.springframework.batch.core.explore.JobExplorer; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.fail; + +public class BaseBatchJobR4Test extends BaseJpaR4Test { + + private static final Logger ourLog = LoggerFactory.getLogger(BaseBatchJobR4Test.class); + @Autowired + private JobExplorer myJobExplorer; + + protected List awaitAllBulkJobCompletions(String... theJobNames) { + assert theJobNames.length > 0; + + List bulkExport = new ArrayList<>(); + for (String nextName : theJobNames) { + bulkExport.addAll(myJobExplorer.findJobInstancesByJobName(nextName, 0, 100)); + } + if (bulkExport.isEmpty()) { + List wantNames = Arrays.asList(theJobNames); + List haveNames = myJobExplorer.getJobNames(); + fail("There are no jobs running - Want names " + wantNames + " and have names " + haveNames); + } + List bulkExportExecutions = bulkExport.stream().flatMap(jobInstance -> myJobExplorer.getJobExecutions(jobInstance).stream()).collect(Collectors.toList()); + awaitJobCompletions(bulkExportExecutions); + + return bulkExportExecutions; + } + + protected void awaitJobCompletions(Collection theJobs) { + theJobs.forEach(jobExecution -> awaitJobCompletion(jobExecution)); + } + + protected void awaitJobCompletion(JobExecution theJobExecution) { + await().atMost(120, TimeUnit.SECONDS).until(() -> { + JobExecution jobExecution = myJobExplorer.getJobExecution(theJobExecution.getId()); + ourLog.info("JobExecution {} currently has status: {}- Failures if any: {}", theJobExecution.getId(), jobExecution.getStatus(), jobExecution.getFailureExceptions()); + return jobExecution.getStatus() == BatchStatus.COMPLETED || jobExecution.getStatus() == BatchStatus.FAILED; + }); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderTest.java index ede41a213e8..2c216b9074d 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportProviderTest.java @@ -2,11 +2,11 @@ package ca.uhn.fhir.jpa.bulk; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.jpa.bulk.api.BulkDataExportOptions; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; -import ca.uhn.fhir.jpa.bulk.model.BulkExportResponseJson; -import ca.uhn.fhir.jpa.bulk.model.BulkJobStatusEnum; -import ca.uhn.fhir.jpa.bulk.provider.BulkDataExportProvider; +import ca.uhn.fhir.jpa.bulk.export.api.BulkDataExportOptions; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.model.BulkExportResponseJson; +import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum; +import ca.uhn.fhir.jpa.bulk.export.provider.BulkDataExportProvider; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.client.apache.ResourceEntity; @@ -188,7 +188,7 @@ public class BulkDataExportProviderTest { IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo() .setJobId(A_JOB_ID) - .setStatus(BulkJobStatusEnum.BUILDING) + .setStatus(BulkExportJobStatusEnum.BUILDING) .setStatusTime(InstantType.now().getValue()); when(myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(eq(A_JOB_ID))).thenReturn(jobInfo); @@ -212,7 +212,7 @@ public class BulkDataExportProviderTest { IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo() .setJobId(A_JOB_ID) - .setStatus(BulkJobStatusEnum.ERROR) + .setStatus(BulkExportJobStatusEnum.ERROR) .setStatusTime(InstantType.now().getValue()) .setStatusMessage("Some Error Message"); when(myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(eq(A_JOB_ID))).thenReturn(jobInfo); @@ -239,7 +239,7 @@ public class BulkDataExportProviderTest { IBulkDataExportSvc.JobInfo jobInfo = new IBulkDataExportSvc.JobInfo() .setJobId(A_JOB_ID) - .setStatus(BulkJobStatusEnum.COMPLETE) + .setStatus(BulkExportJobStatusEnum.COMPLETE) .setStatusTime(InstantType.now().getValue()); jobInfo.addFile().setResourceType("Patient").setResourceId(new IdType("Binary/111")); jobInfo.addFile().setResourceType("Patient").setResourceId(new IdType("Binary/222")); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java index 58fd8a39f5b..fdc92d090e9 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java @@ -6,15 +6,14 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.batch.BatchJobsConfig; import ca.uhn.fhir.jpa.batch.api.IBatchJobSubmitter; -import ca.uhn.fhir.jpa.bulk.api.BulkDataExportOptions; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; -import ca.uhn.fhir.jpa.bulk.job.BulkExportJobParametersBuilder; -import ca.uhn.fhir.jpa.bulk.job.GroupBulkExportJobParametersBuilder; -import ca.uhn.fhir.jpa.bulk.model.BulkJobStatusEnum; +import ca.uhn.fhir.jpa.bulk.export.api.BulkDataExportOptions; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.job.BulkExportJobParametersBuilder; +import ca.uhn.fhir.jpa.bulk.export.job.GroupBulkExportJobParametersBuilder; +import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum; import ca.uhn.fhir.jpa.dao.data.IBulkExportCollectionDao; import ca.uhn.fhir.jpa.dao.data.IBulkExportCollectionFileDao; import ca.uhn.fhir.jpa.dao.data.IBulkExportJobDao; -import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; import ca.uhn.fhir.jpa.entity.BulkExportCollectionEntity; import ca.uhn.fhir.jpa.entity.BulkExportCollectionFileEntity; import ca.uhn.fhir.jpa.entity.BulkExportJobEntity; @@ -46,28 +45,22 @@ import org.hl7.fhir.r4.model.Reference; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.Job; import org.springframework.batch.core.JobExecution; -import org.springframework.batch.core.JobInstance; import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.JobParametersInvalidException; -import org.springframework.batch.core.explore.JobExplorer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import java.util.Arrays; -import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.awaitility.Awaitility.await; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -78,7 +71,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { +public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { public static final String TEST_FILTER = "Patient?gender=female"; private static final Logger ourLog = LoggerFactory.getLogger(BulkDataExportSvcImplR4Test.class); @@ -92,8 +85,6 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { private IBulkDataExportSvc myBulkDataExportSvc; @Autowired private IBatchJobSubmitter myBatchJobSubmitter; - @Autowired - private JobExplorer myJobExplorer; @Autowired @Qualifier(BatchJobsConfig.BULK_EXPORT_JOB_NAME) @@ -128,7 +119,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { String binaryId = myBinaryDao.create(b).getId().toUnqualifiedVersionless().getValue(); BulkExportJobEntity job = new BulkExportJobEntity(); - job.setStatus(BulkJobStatusEnum.COMPLETE); + job.setStatus(BulkExportJobStatusEnum.COMPLETE); job.setExpiry(DateUtils.addHours(new Date(), -1)); job.setJobId(UUID.randomUUID().toString()); job.setCreated(new Date()); @@ -241,6 +232,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { options.setExportStyle(BulkDataExportOptions.ExportStyle.SYSTEM); return options; } + @Test public void testSubmit_ReusesExisting() { @@ -278,7 +270,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { // Check the status IBulkDataExportSvc.JobInfo status = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertEquals(BulkJobStatusEnum.SUBMITTED, status.getStatus()); + assertEquals(BulkExportJobStatusEnum.SUBMITTED, status.getStatus()); // Run a scheduled pass to build the export myBulkDataExportSvc.buildExportFiles(); @@ -287,7 +279,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { // Fetch the job again status = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertEquals(BulkJobStatusEnum.ERROR, status.getStatus()); + assertEquals(BulkExportJobStatusEnum.ERROR, status.getStatus()); assertThat(status.getStatusMessage(), containsString("help i'm a bug")); } finally { @@ -295,6 +287,14 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { } } + private void awaitAllBulkJobCompletions() { + awaitAllBulkJobCompletions( + BatchJobsConfig.BULK_EXPORT_JOB_NAME, + BatchJobsConfig.PATIENT_BULK_EXPORT_JOB_NAME, + BatchJobsConfig.GROUP_BULK_EXPORT_JOB_NAME + ); + } + @Test public void testGenerateBulkExport_SpecificResources() { @@ -313,7 +313,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { // Check the status IBulkDataExportSvc.JobInfo status = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertEquals(BulkJobStatusEnum.SUBMITTED, status.getStatus()); + assertEquals(BulkExportJobStatusEnum.SUBMITTED, status.getStatus()); assertEquals("/$export?_outputFormat=application%2Ffhir%2Bndjson&_type=Observation,Patient&_typeFilter=" + UrlUtil.escapeUrlParam(TEST_FILTER), status.getRequest()); // Run a scheduled pass to build the export @@ -323,7 +323,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { // Fetch the job again status = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertEquals(BulkJobStatusEnum.COMPLETE, status.getStatus()); + assertEquals(BulkExportJobStatusEnum.COMPLETE, status.getStatus()); // Iterate over the files for (IBulkDataExportSvc.FileEntry next : status.getFiles()) { @@ -368,7 +368,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { // Check the status IBulkDataExportSvc.JobInfo status = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertEquals(BulkJobStatusEnum.SUBMITTED, status.getStatus()); + assertEquals(BulkExportJobStatusEnum.SUBMITTED, status.getStatus()); assertEquals("/$export?_outputFormat=application%2Ffhir%2Bndjson", status.getRequest()); // Run a scheduled pass to build the export @@ -378,7 +378,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { // Fetch the job again status = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertEquals(BulkJobStatusEnum.COMPLETE, status.getStatus()); + assertEquals(BulkExportJobStatusEnum.COMPLETE, status.getStatus()); assertEquals(5, status.getFiles().size()); // Iterate over the files @@ -393,7 +393,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { } else if ("Observation".equals(next.getResourceType())) { assertThat(nextContents, containsString("\"subject\":{\"reference\":\"Patient/PAT0\"}}\n")); assertEquals(26, nextContents.split("\n").length); - }else if ("Immunization".equals(next.getResourceType())) { + } else if ("Immunization".equals(next.getResourceType())) { assertThat(nextContents, containsString("\"patient\":{\"reference\":\"Patient/PAT0\"}}\n")); assertEquals(26, nextContents.split("\n").length); } else if ("CareTeam".equals(next.getResourceType())) { @@ -428,7 +428,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertThat(jobInfo.getStatus(), equalTo(BulkJobStatusEnum.COMPLETE)); + assertThat(jobInfo.getStatus(), equalTo(BulkExportJobStatusEnum.COMPLETE)); assertThat(jobInfo.getFiles().size(), equalTo(5)); } @@ -451,7 +451,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { // Check the status IBulkDataExportSvc.JobInfo status = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertEquals(BulkJobStatusEnum.SUBMITTED, status.getStatus()); + assertEquals(BulkExportJobStatusEnum.SUBMITTED, status.getStatus()); assertEquals("/$export?_outputFormat=application%2Ffhir%2Bndjson&_type=Patient&_typeFilter=Patient%3F_has%3AObservation%3Apatient%3Aidentifier%3DSYS%7CVAL3", status.getRequest()); // Run a scheduled pass to build the export @@ -461,7 +461,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { // Fetch the job again status = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertEquals(BulkJobStatusEnum.COMPLETE, status.getStatus()); + assertEquals(BulkExportJobStatusEnum.COMPLETE, status.getStatus()); assertEquals(1, status.getFiles().size()); // Iterate over the files @@ -481,7 +481,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { } @Test - public void testGenerateBulkExport_WithSince() throws InterruptedException { + public void testGenerateBulkExport_WithSince() { // Create some resources to load createResources(); @@ -508,7 +508,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { // Check the status IBulkDataExportSvc.JobInfo status = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertEquals(BulkJobStatusEnum.SUBMITTED, status.getStatus()); + assertEquals(BulkExportJobStatusEnum.SUBMITTED, status.getStatus()); assertEquals("/$export?_outputFormat=application%2Ffhir%2Bndjson&_type=Observation,Patient&_since=" + cutoff.setTimeZoneZulu(true).getValueAsString(), status.getRequest()); // Run a scheduled pass to build the export @@ -518,7 +518,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { // Fetch the job again status = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertEquals(BulkJobStatusEnum.COMPLETE, status.getStatus()); + assertEquals(BulkExportJobStatusEnum.COMPLETE, status.getStatus()); assertEquals(1, status.getFiles().size()); // Iterate over the files @@ -560,24 +560,10 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { String jobUUID = (String) jobExecution.getExecutionContext().get("jobUUID"); IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobUUID); - assertThat(jobInfo.getStatus(), equalTo(BulkJobStatusEnum.COMPLETE)); + assertThat(jobInfo.getStatus(), equalTo(BulkExportJobStatusEnum.COMPLETE)); assertThat(jobInfo.getFiles().size(), equalTo(2)); } - public void awaitAllBulkJobCompletions() { - List bulkExport = myJobExplorer.findJobInstancesByJobName(BatchJobsConfig.BULK_EXPORT_JOB_NAME, 0, 100); - bulkExport.addAll(myJobExplorer.findJobInstancesByJobName(BatchJobsConfig.PATIENT_BULK_EXPORT_JOB_NAME, 0, 100)); - bulkExport.addAll(myJobExplorer.findJobInstancesByJobName(BatchJobsConfig.GROUP_BULK_EXPORT_JOB_NAME, 0, 100)); - if (bulkExport.isEmpty()) { - fail("There are no bulk export jobs running!"); - } - List bulkExportExecutions = bulkExport.stream().flatMap(jobInstance -> myJobExplorer.getJobExecutions(jobInstance).stream()).collect(Collectors.toList()); - awaitJobCompletions(bulkExportExecutions); - } - - public void awaitJobCompletions(Collection theJobs) { - theJobs.forEach(jobExecution -> awaitJobCompletion(jobExecution)); - } @Test public void testBatchJobSubmitsAndRuns() throws Exception { @@ -599,13 +585,13 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { awaitJobCompletion(jobExecution); IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertThat(jobInfo.getStatus(), equalTo(BulkJobStatusEnum.COMPLETE)); + assertThat(jobInfo.getStatus(), equalTo(BulkExportJobStatusEnum.COMPLETE)); assertThat(jobInfo.getFiles().size(), equalTo(2)); } @Test - public void testGroupBatchJobWorks() throws Exception { + public void testGroupBatchJobWorks() { createResources(); // Create a bulk job @@ -625,7 +611,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertThat(jobInfo.getStatus(), equalTo(BulkJobStatusEnum.COMPLETE)); + assertThat(jobInfo.getStatus(), equalTo(BulkExportJobStatusEnum.COMPLETE)); assertThat(jobInfo.getFiles().size(), equalTo(1)); assertThat(jobInfo.getFiles().get(0).getResourceType(), is(equalTo("Immunization"))); @@ -639,8 +625,9 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { assertThat(nextContents, is(containsString("IMM6"))); assertThat(nextContents, is(containsString("IMM8"))); } + @Test - public void testGroupBatchJobMdmExpansionIdentifiesGoldenResources() throws Exception { + public void testGroupBatchJobMdmExpansionIdentifiesGoldenResources() { createResources(); // Create a bulk job @@ -659,7 +646,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertThat(jobInfo.getStatus(), equalTo(BulkJobStatusEnum.COMPLETE)); + assertThat(jobInfo.getStatus(), equalTo(BulkExportJobStatusEnum.COMPLETE)); assertThat(jobInfo.getFiles().size(), equalTo(2)); assertThat(jobInfo.getFiles().get(0).getResourceType(), is(equalTo("Immunization"))); @@ -716,7 +703,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { awaitJobCompletion(jobExecution); IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertThat(jobInfo.getStatus(), equalTo(BulkJobStatusEnum.COMPLETE)); + assertThat(jobInfo.getStatus(), equalTo(BulkExportJobStatusEnum.COMPLETE)); assertThat(jobInfo.getFiles().size(), equalTo(2)); assertThat(jobInfo.getFiles().get(0).getResourceType(), is(equalTo("Immunization"))); @@ -747,7 +734,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { // CareTeam has two patient references: participant and patient. This test checks if we find the patient if participant is null but patient is not null @Test - public void testGroupBatchJobCareTeam() throws Exception { + public void testGroupBatchJobCareTeam() { createResources(); BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions(); @@ -766,7 +753,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertThat(jobInfo.getStatus(), equalTo(BulkJobStatusEnum.COMPLETE)); + assertThat(jobInfo.getStatus(), equalTo(BulkExportJobStatusEnum.COMPLETE)); assertThat(jobInfo.getFiles().size(), equalTo(1)); assertThat(jobInfo.getFiles().get(0).getResourceType(), is(equalTo("CareTeam"))); @@ -810,7 +797,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertThat(jobInfo.getStatus(), equalTo(BulkJobStatusEnum.COMPLETE)); + assertThat(jobInfo.getStatus(), equalTo(BulkExportJobStatusEnum.COMPLETE)); assertThat(jobInfo.getFiles().size(), equalTo(1)); assertThat(jobInfo.getFiles().get(0).getResourceType(), is(equalTo("Immunization"))); @@ -847,7 +834,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertThat(jobInfo.getStatus(), equalTo(BulkJobStatusEnum.COMPLETE)); + assertThat(jobInfo.getStatus(), equalTo(BulkExportJobStatusEnum.COMPLETE)); assertThat(jobInfo.getFiles().size(), equalTo(1)); assertThat(jobInfo.getFiles().get(0).getResourceType(), is(equalTo("Observation"))); String nextContents = getBinaryContents(jobInfo, 0); @@ -888,7 +875,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { awaitAllBulkJobCompletions(); IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertThat(jobInfo.getStatus(), equalTo(BulkJobStatusEnum.COMPLETE)); + assertThat(jobInfo.getStatus(), equalTo(BulkExportJobStatusEnum.COMPLETE)); assertThat(jobInfo.getFiles().size(), equalTo(1)); assertThat(jobInfo.getFiles().get(0).getResourceType(), is(equalTo("Patient"))); @@ -900,7 +887,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { } @Test - public void testMdmExpansionWorksForGroupExportOnMatchedPatients() throws JobParametersInvalidException { + public void testMdmExpansionWorksForGroupExportOnMatchedPatients() { createResources(); // Create a bulk job @@ -918,9 +905,9 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { awaitAllBulkJobCompletions(); IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertEquals("/Group/G0/$export?_outputFormat=application%2Ffhir%2Bndjson&_type=Observation,Immunization&_groupId=" + myPatientGroupId +"&_mdm=true", jobInfo.getRequest()); + assertEquals("/Group/G0/$export?_outputFormat=application%2Ffhir%2Bndjson&_type=Observation,Immunization&_groupId=" + myPatientGroupId + "&_mdm=true", jobInfo.getRequest()); - assertThat(jobInfo.getStatus(), equalTo(BulkJobStatusEnum.COMPLETE)); + assertThat(jobInfo.getStatus(), equalTo(BulkExportJobStatusEnum.COMPLETE)); assertThat(jobInfo.getFiles().size(), equalTo(2)); assertThat(jobInfo.getFiles().get(0).getResourceType(), is(equalTo("Immunization"))); @@ -963,7 +950,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { } @Test - public void testGroupBulkExportSupportsTypeFilters() throws JobParametersInvalidException { + public void testGroupBulkExportSupportsTypeFilters() { createResources(); //Only get COVID-19 vaccinations @@ -985,7 +972,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertThat(jobInfo.getStatus(), equalTo(BulkJobStatusEnum.COMPLETE)); + assertThat(jobInfo.getStatus(), equalTo(BulkExportJobStatusEnum.COMPLETE)); assertThat(jobInfo.getFiles().size(), equalTo(1)); assertThat(jobInfo.getFiles().get(0).getResourceType(), is(equalTo("Immunization"))); @@ -1021,7 +1008,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { myBulkDataExportSvc.buildExportFiles(); awaitAllBulkJobCompletions(); IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertThat(jobInfo.getStatus(), is(equalTo(BulkJobStatusEnum.COMPLETE))); + assertThat(jobInfo.getStatus(), is(equalTo(BulkExportJobStatusEnum.COMPLETE))); //Group-style bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.GROUP); @@ -1030,7 +1017,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { myBulkDataExportSvc.buildExportFiles(); awaitAllBulkJobCompletions(); jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertThat(jobInfo.getStatus(), is(equalTo(BulkJobStatusEnum.COMPLETE))); + assertThat(jobInfo.getStatus(), is(equalTo(BulkExportJobStatusEnum.COMPLETE))); //System-style bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.SYSTEM); @@ -1038,7 +1025,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { myBulkDataExportSvc.buildExportFiles(); awaitAllBulkJobCompletions(); jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); - assertThat(jobInfo.getStatus(), is(equalTo(BulkJobStatusEnum.COMPLETE))); + assertThat(jobInfo.getStatus(), is(equalTo(BulkExportJobStatusEnum.COMPLETE))); } @Test @@ -1077,14 +1064,6 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { } - private void awaitJobCompletion(JobExecution theJobExecution) { - await().atMost(120, TimeUnit.SECONDS).until(() -> { - JobExecution jobExecution = myJobExplorer.getJobExecution(theJobExecution.getId()); - ourLog.info("JobExecution {} currently has status: {}", theJobExecution.getId(), jobExecution.getStatus()); - return jobExecution.getStatus() == BatchStatus.COMPLETED || jobExecution.getStatus() == BatchStatus.FAILED; - }); - } - private void createResources() { Group group = new Group(); group.setId("G0"); @@ -1109,7 +1088,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { linkToGoldenResource(goldenPid, sourcePid); //Only add half the patients to the group. - if (i % 2 == 0 ) { + if (i % 2 == 0) { group.addMember().setEntity(new Reference(patId)); } @@ -1119,7 +1098,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { createCareTeamWithIndex(i, patId); } - myPatientGroupId = myGroupDao.update(group).getId(); + myPatientGroupId = myGroupDao.update(group).getId(); //Manually create another golden record Patient goldenPatient2 = new Patient(); @@ -1153,8 +1132,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { patient.setGender(i % 2 == 0 ? Enumerations.AdministrativeGender.MALE : Enumerations.AdministrativeGender.FEMALE); patient.addName().setFamily("FAM" + i); patient.addIdentifier().setSystem("http://mrns").setValue("PAT" + i); - DaoMethodOutcome patientOutcome = myPatientDao.update(patient); - return patientOutcome; + return myPatientDao.update(patient); } private void createCareTeamWithIndex(int i, IIdType patId) { @@ -1167,7 +1145,7 @@ public class BulkDataExportSvcImplR4Test extends BaseJpaR4Test { private void createImmunizationWithIndex(int i, IIdType patId) { Immunization immunization = new Immunization(); immunization.setId("IMM" + i); - if (patId != null ) { + if (patId != null) { immunization.setPatient(new Reference(patId)); } if (i % 2 == 0) { diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportR4Test.java new file mode 100644 index 00000000000..dcee246154c --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportR4Test.java @@ -0,0 +1,155 @@ +package ca.uhn.fhir.jpa.bulk.imprt.svc; + +import ca.uhn.fhir.interceptor.api.HookParams; +import ca.uhn.fhir.interceptor.api.IAnonymousInterceptor; +import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.jpa.batch.BatchJobsConfig; +import ca.uhn.fhir.jpa.bulk.BaseBatchJobR4Test; +import ca.uhn.fhir.jpa.bulk.export.job.BulkExportJobConfig; +import ca.uhn.fhir.jpa.bulk.imprt.api.IBulkDataImportSvc; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobFileJson; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobJson; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum; +import ca.uhn.fhir.jpa.bulk.imprt.model.JobFileRowProcessingModeEnum; +import ca.uhn.fhir.jpa.dao.data.IBulkImportJobDao; +import ca.uhn.fhir.jpa.dao.data.IBulkImportJobFileDao; +import ca.uhn.fhir.jpa.entity.BulkImportJobEntity; +import ca.uhn.fhir.jpa.entity.BulkImportJobFileEntity; +import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.test.utilities.ITestDataBuilder; +import ca.uhn.fhir.util.BundleBuilder; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.batch.core.JobExecution; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class BulkDataImportR4Test extends BaseBatchJobR4Test implements ITestDataBuilder { + + @Autowired + private IBulkDataImportSvc mySvc; + @Autowired + private IBulkImportJobDao myBulkImportJobDao; + @Autowired + private IBulkImportJobFileDao myBulkImportJobFileDao; + + @AfterEach + public void after() { + myInterceptorRegistry.unregisterInterceptorsIf(t -> t instanceof IAnonymousInterceptor); + } + + @Test + public void testFlow_TransactionRows() { + int transactionsPerFile = 10; + int fileCount = 10; + List files = createInputFiles(transactionsPerFile, fileCount); + + BulkImportJobJson job = new BulkImportJobJson(); + job.setProcessingMode(JobFileRowProcessingModeEnum.FHIR_TRANSACTION); + job.setJobDescription("This is the description"); + job.setBatchSize(3); + String jobId = mySvc.createNewJob(job, files); + mySvc.markJobAsReadyForActivation(jobId); + + boolean activateJobOutcome = mySvc.activateNextReadyJob(); + assertTrue(activateJobOutcome); + + List executions = awaitAllBulkJobCompletions(); + assertEquals(1, executions.size()); + assertEquals("This is the description", executions.get(0).getJobParameters().getString(BulkExportJobConfig.JOB_DESCRIPTION)); + + runInTransaction(() -> { + List jobs = myBulkImportJobDao.findAll(); + assertEquals(0, jobs.size()); + + List jobFiles = myBulkImportJobFileDao.findAll(); + assertEquals(0, jobFiles.size()); + + }); + + IBundleProvider searchResults = myPatientDao.search(SearchParameterMap.newSynchronous()); + assertEquals(transactionsPerFile * fileCount, searchResults.sizeOrThrowNpe()); + + } + + @Test + public void testFlow_WithTenantNamesInInput() { + int transactionsPerFile = 5; + int fileCount = 10; + List files = createInputFiles(transactionsPerFile, fileCount); + for (int i = 0; i < fileCount; i++) { + files.get(i).setTenantName("TENANT" + i); + } + + IAnonymousInterceptor interceptor = mock(IAnonymousInterceptor.class); + myInterceptorRegistry.registerAnonymousInterceptor(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED, interceptor); + + BulkImportJobJson job = new BulkImportJobJson(); + job.setProcessingMode(JobFileRowProcessingModeEnum.FHIR_TRANSACTION); + job.setBatchSize(5); + String jobId = mySvc.createNewJob(job, files); + mySvc.markJobAsReadyForActivation(jobId); + + boolean activateJobOutcome = mySvc.activateNextReadyJob(); + assertTrue(activateJobOutcome); + + awaitAllBulkJobCompletions(); + + ArgumentCaptor paramsCaptor = ArgumentCaptor.forClass(HookParams.class); + verify(interceptor, times(50)).invoke(any(), paramsCaptor.capture()); + List tenantNames = paramsCaptor + .getAllValues() + .stream() + .map(t -> t.get(RequestDetails.class).getTenantId()) + .distinct() + .sorted() + .collect(Collectors.toList()); + assertThat(tenantNames, containsInAnyOrder( + "TENANT0", "TENANT1", "TENANT2", "TENANT3", "TENANT4", "TENANT5", "TENANT6", "TENANT7", "TENANT8", "TENANT9" + )); + } + + + @Nonnull + private List createInputFiles(int transactionsPerFile, int fileCount) { + List files = new ArrayList<>(); + for (int fileIndex = 0; fileIndex < fileCount; fileIndex++) { + StringBuilder fileContents = new StringBuilder(); + + for (int transactionIdx = 0; transactionIdx < transactionsPerFile; transactionIdx++) { + BundleBuilder bundleBuilder = new BundleBuilder(myFhirCtx); + IBaseResource patient = buildPatient(withFamily("FAM " + fileIndex + " " + transactionIdx)); + bundleBuilder.addTransactionCreateEntry(patient); + fileContents.append(myFhirCtx.newJsonParser().setPrettyPrint(false).encodeResourceToString(bundleBuilder.getBundle())); + fileContents.append("\n"); + } + + BulkImportJobFileJson nextFile = new BulkImportJobFileJson(); + nextFile.setContents(fileContents.toString()); + files.add(nextFile); + } + return files; + } + + protected List awaitAllBulkJobCompletions() { + return awaitAllBulkJobCompletions(BatchJobsConfig.BULK_IMPORT_JOB_NAME); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImplTest.java new file mode 100644 index 00000000000..5bc80f28024 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/imprt/svc/BulkDataImportSvcImplTest.java @@ -0,0 +1,145 @@ +package ca.uhn.fhir.jpa.bulk.imprt.svc; + +import ca.uhn.fhir.jpa.bulk.imprt.api.IBulkDataImportSvc; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobFileJson; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobJson; +import ca.uhn.fhir.jpa.bulk.imprt.model.BulkImportJobStatusEnum; +import ca.uhn.fhir.jpa.bulk.imprt.model.JobFileRowProcessingModeEnum; +import ca.uhn.fhir.jpa.dao.data.IBulkImportJobDao; +import ca.uhn.fhir.jpa.dao.data.IBulkImportJobFileDao; +import ca.uhn.fhir.jpa.dao.r4.BaseJpaR4Test; +import ca.uhn.fhir.jpa.entity.BulkImportJobEntity; +import ca.uhn.fhir.jpa.entity.BulkImportJobFileEntity; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.blankString; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BulkDataImportSvcImplTest extends BaseJpaR4Test { + + @Autowired + private IBulkDataImportSvc mySvc; + @Autowired + private IBulkImportJobDao myBulkImportJobDao; + @Autowired + private IBulkImportJobFileDao myBulkImportJobFileDao; + + @Test + public void testCreateNewJob() { + + // Create job + BulkImportJobJson job = new BulkImportJobJson(); + job.setProcessingMode(JobFileRowProcessingModeEnum.FHIR_TRANSACTION); + job.setBatchSize(3); + BulkImportJobFileJson file1 = new BulkImportJobFileJson(); + file1.setContents("contents 1"); + BulkImportJobFileJson file2 = new BulkImportJobFileJson(); + file2.setContents("contents 2"); + String jobId = mySvc.createNewJob(job, Lists.newArrayList(file1, file2)); + assertThat(jobId, not(blankString())); + + // Add file + BulkImportJobFileJson file3 = new BulkImportJobFileJson(); + file3.setContents("contents 3"); + mySvc.addFilesToJob(jobId, Lists.newArrayList(file3)); + + runInTransaction(() -> { + List jobs = myBulkImportJobDao.findAll(); + assertEquals(1, jobs.size()); + assertEquals(jobId, jobs.get(0).getJobId()); + assertEquals(3, jobs.get(0).getFileCount()); + assertEquals(BulkImportJobStatusEnum.STAGING, jobs.get(0).getStatus()); + + List files = myBulkImportJobFileDao.findAllForJob(jobId); + assertEquals(3, files.size()); + + }); + } + + @Test + public void testCreateNewJob_InvalidJob_NoContents() { + BulkImportJobJson job = new BulkImportJobJson(); + job.setProcessingMode(JobFileRowProcessingModeEnum.FHIR_TRANSACTION); + job.setBatchSize(3); + BulkImportJobFileJson file1 = new BulkImportJobFileJson(); + try { + mySvc.createNewJob(job, Lists.newArrayList(file1)); + } catch (UnprocessableEntityException e) { + assertEquals("Job File Contents mode must not be null", e.getMessage()); + } + } + + @Test + public void testCreateNewJob_InvalidJob_NoProcessingMode() { + BulkImportJobJson job = new BulkImportJobJson(); + job.setProcessingMode(JobFileRowProcessingModeEnum.FHIR_TRANSACTION); + job.setBatchSize(3); + BulkImportJobFileJson file1 = new BulkImportJobFileJson(); + file1.setContents("contents 1"); + try { + mySvc.createNewJob(job, Lists.newArrayList(file1)); + } catch (UnprocessableEntityException e) { + assertEquals("Job File Processing mode must not be null", e.getMessage()); + } + } + + @Test + public void testAddFilesToJob_InvalidId() { + BulkImportJobFileJson file3 = new BulkImportJobFileJson(); + file3.setContents("contents 3"); + try { + mySvc.addFilesToJob("ABCDEFG", Lists.newArrayList(file3)); + } catch (InvalidRequestException e) { + assertEquals("Unknown job ID: ABCDEFG", e.getMessage()); + } + } + + @Test + public void testAddFilesToJob_WrongStatus() { + runInTransaction(() -> { + BulkImportJobEntity entity = new BulkImportJobEntity(); + entity.setFileCount(1); + entity.setJobId("ABCDEFG"); + entity.setStatus(BulkImportJobStatusEnum.RUNNING); + entity.setRowProcessingMode(JobFileRowProcessingModeEnum.FHIR_TRANSACTION); + myBulkImportJobDao.save(entity); + }); + + BulkImportJobFileJson file3 = new BulkImportJobFileJson(); + file3.setContents("contents 3"); + try { + mySvc.addFilesToJob("ABCDEFG", Lists.newArrayList(file3)); + } catch (InvalidRequestException e) { + assertEquals("Job ABCDEFG has status RUNNING and can not be added to", e.getMessage()); + } + } + + @Test + public void testActivateJob() { + runInTransaction(() -> { + BulkImportJobEntity entity = new BulkImportJobEntity(); + entity.setFileCount(1); + entity.setJobId("ABCDEFG"); + entity.setStatus(BulkImportJobStatusEnum.STAGING); + entity.setRowProcessingMode(JobFileRowProcessingModeEnum.FHIR_TRANSACTION); + myBulkImportJobDao.save(entity); + }); + + mySvc.markJobAsReadyForActivation("ABCDEFG"); + + runInTransaction(() -> { + List jobs = myBulkImportJobDao.findAll(); + assertEquals(1, jobs.size()); + assertEquals(BulkImportJobStatusEnum.READY, jobs.get(0).getStatus()); + }); + } + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java index 8f22d6dda97..83c339af0bf 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/BaseJpaTest.java @@ -9,7 +9,7 @@ import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.api.model.ExpungeOptions; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; import ca.uhn.fhir.jpa.config.BaseConfig; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.entity.TermConcept; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TransactionProcessorTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TransactionProcessorTest.java index 63f61b740b1..301f5476e7a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TransactionProcessorTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/TransactionProcessorTest.java @@ -85,7 +85,7 @@ public class TransactionProcessorTest { .setUrl("/MedicationKnowledge"); try { - myTransactionProcessor.transaction(null, input); + myTransactionProcessor.transaction(null, input, false); fail(); } catch (InvalidRequestException e) { assertEquals("Resource MedicationKnowledge is not supported on this server. Supported resource types: []", e.getMessage()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java index 483e0874f02..5992f9df117 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/BaseJpaDstu2Test.java @@ -8,7 +8,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoSubscription; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; import ca.uhn.fhir.jpa.config.TestDstu2Config; import ca.uhn.fhir.jpa.dao.BaseJpaTest; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java index 9cd366f3b1e..e4803f648d5 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu2/FhirSystemDaoDstu2Test.java @@ -785,7 +785,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaDstu2SystemTest { // try { Bundle resp = mySystemDao.transaction(mySrd, request); assertEquals(1, resp.getEntry().size()); - assertEquals("404 Not Found", resp.getEntry().get(0).getResponse().getStatus()); + assertEquals("204 No Content", resp.getEntry().get(0).getResponse().getStatus()); // fail(); // } catch (ResourceNotFoundException e) { @@ -1159,11 +1159,7 @@ public class FhirSystemDaoDstu2Test extends BaseJpaDstu2SystemTest { } assertEquals("201 Created", resp.getEntry().get(2).getResponse().getStatus()); assertThat(resp.getEntry().get(2).getResponse().getLocation(), startsWith("Patient/")); - if (pass == 0) { - assertEquals("404 Not Found", resp.getEntry().get(3).getResponse().getStatus()); - } else { - assertEquals("204 No Content", resp.getEntry().get(3).getResponse().getStatus()); - } + assertEquals("204 No Content", resp.getEntry().get(3).getResponse().getStatus()); Bundle respGetBundle = (Bundle) resp.getEntry().get(0).getResource(); assertEquals(1, respGetBundle.getEntry().size()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java index 109a59cdad0..0953336ff66 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/BaseJpaDstu3Test.java @@ -13,7 +13,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.batch.api.IBatchJobSubmitter; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; import ca.uhn.fhir.jpa.config.TestDstu3Config; import ca.uhn.fhir.jpa.dao.BaseJpaTest; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java index bf257e1f5ed..9cb19833adf 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseJpaR4Test.java @@ -17,7 +17,7 @@ import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.batch.api.IBatchJobSubmitter; import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider; import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; import ca.uhn.fhir.jpa.config.TestR4Config; import ca.uhn.fhir.jpa.dao.BaseJpaTest; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; @@ -51,24 +51,19 @@ import ca.uhn.fhir.jpa.dao.data.ITermConceptParentChildLinkDao; import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDao; import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDesignationDao; import ca.uhn.fhir.jpa.dao.data.ITermValueSetDao; -import ca.uhn.fhir.jpa.dao.dstu2.FhirResourceDaoDstu2SearchNoFtTest; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.entity.TermCodeSystem; import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion; import ca.uhn.fhir.jpa.entity.TermConcept; import ca.uhn.fhir.jpa.entity.TermValueSet; import ca.uhn.fhir.jpa.entity.TermValueSetConcept; -import ca.uhn.fhir.jpa.entity.TermValueSetConceptDesignation; import ca.uhn.fhir.jpa.interceptor.PerformanceTracingLoggingInterceptor; import ca.uhn.fhir.jpa.model.config.PartitionSettings; import ca.uhn.fhir.jpa.model.entity.ModelConfig; -import ca.uhn.fhir.jpa.model.entity.ResourceIndexedSearchParamString; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.packages.IPackageInstallerSvc; import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc; -import ca.uhn.fhir.jpa.provider.r4.BaseJpaResourceProviderObservationR4; import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4; -import ca.uhn.fhir.jpa.rp.r4.ObservationResourceProvider; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.jpa.search.IStaleSearchDeletingSvc; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; @@ -77,7 +72,6 @@ import ca.uhn.fhir.jpa.searchparam.registry.SearchParamRegistryImpl; import ca.uhn.fhir.jpa.term.BaseTermReadSvcImpl; import ca.uhn.fhir.jpa.term.TermConceptMappingSvcImpl; import ca.uhn.fhir.jpa.term.TermDeferredStorageSvcImpl; -import ca.uhn.fhir.jpa.term.ValueSetExpansionR4Test; import ca.uhn.fhir.jpa.term.api.ITermCodeSystemStorageSvc; import ca.uhn.fhir.jpa.term.api.ITermConceptMappingSvc; import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc; @@ -95,11 +89,9 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.provider.ResourceProviderFactory; import ca.uhn.fhir.test.utilities.ITestDataBuilder; import ca.uhn.fhir.util.ClasspathUtil; -import ca.uhn.fhir.util.ResourceUtil; import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ValidationResult; -import org.apache.commons.io.IOUtils; import org.hibernate.search.mapper.orm.Search; import org.hibernate.search.mapper.orm.session.SearchSession; import org.hl7.fhir.common.hapi.validation.support.CachingValidationSupport; @@ -168,7 +160,6 @@ import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r5.utils.IResourceValidator; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -182,7 +173,6 @@ import org.springframework.transaction.PlatformTransactionManager; import javax.persistence.EntityManager; import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Map; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java index 7e71ceab8ad..79904f2d15f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithElasticSearchIT.java @@ -39,7 +39,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; import ca.uhn.fhir.jpa.config.TestR4ConfigWithElasticSearch; import ca.uhn.fhir.jpa.dao.BaseJpaTest; import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithLuceneDisabledTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithLuceneDisabledTest.java index 4f0eb913e03..d01756a7265 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithLuceneDisabledTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchWithLuceneDisabledTest.java @@ -7,7 +7,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; import ca.uhn.fhir.jpa.config.TestR4WithLuceneDisabledConfig; import ca.uhn.fhir.jpa.dao.BaseJpaTest; import ca.uhn.fhir.jpa.dao.dstu2.FhirResourceDaoDstu2SearchNoFtTest; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyElasticsearchIT.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyElasticsearchIT.java index c5ba2df8890..c4b556bc6ed 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyElasticsearchIT.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4TerminologyElasticsearchIT.java @@ -6,7 +6,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; import ca.uhn.fhir.jpa.config.TestR4ConfigWithElasticSearch; import ca.uhn.fhir.jpa.dao.BaseJpaTest; import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java index 24951ce3060..a7d4f7a5278 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirSystemDaoR4Test.java @@ -18,6 +18,7 @@ import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; @@ -60,10 +61,10 @@ import org.hl7.fhir.r4.model.Quantity; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.r4.model.ValueSet; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; @@ -109,8 +110,8 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { public void after() { myDaoConfig.setAllowInlineMatchUrlReferences(false); myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete()); - myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED); - } + myModelConfig.setNormalizedQuantitySearchLevel(NormalizedQuantitySearchLevel.NORMALIZED_QUANTITY_SEARCH_NOT_SUPPORTED); + } @BeforeEach public void beforeDisableResultReuse() { @@ -549,7 +550,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { myValueSetDao.create(vs, mySrd); sleepUntilTimeChanges(); - + ResourceTable entity = new TransactionTemplate(myTxManager).execute(t -> myEntityManager.find(ResourceTable.class, id.getIdPartAsLong())); assertEquals(Long.valueOf(1), entity.getIndexStatus()); @@ -568,9 +569,9 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { * so it indexes the newest resource one more time. It wouldn't be a big deal * if this ever got fixed so that it ends up with 2 instead of 3. */ - runInTransaction(()->{ + runInTransaction(() -> { Optional reindexCount = myResourceReindexJobDao.getReindexCount(jobId); - assertEquals(3, reindexCount.orElseThrow(()->new NullPointerException("No job " + jobId)).intValue()); + assertEquals(3, reindexCount.orElseThrow(() -> new NullPointerException("No job " + jobId)).intValue()); }); // Try making the resource unparseable @@ -626,7 +627,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { searchParamMap.add(Patient.SP_FAMILY, new StringParam("family2")); assertEquals(1, myPatientDao.search(searchParamMap).size().intValue()); - runInTransaction(()->{ + runInTransaction(() -> { ResourceHistoryTable historyEntry = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(id.getIdPartAsLong(), 3); assertNotNull(historyEntry); myResourceHistoryTableDao.delete(historyEntry); @@ -656,7 +657,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { searchParamMap.add(Patient.SP_FAMILY, new StringParam("family1")); assertEquals(1, myPatientDao.search(searchParamMap).size().intValue()); - runInTransaction(()->{ + runInTransaction(() -> { myEntityManager .createQuery("UPDATE ResourceIndexedSearchParamString s SET s.myHashNormalizedPrefix = 0") .executeUpdate(); @@ -671,7 +672,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { myResourceReindexingSvc.markAllResourcesForReindexing(); myResourceReindexingSvc.forceReindexingPass(); - runInTransaction(()->{ + runInTransaction(() -> { ResourceIndexedSearchParamString param = myResourceIndexedSearchParamStringDao.findAll() .stream() .filter(t -> t.getParamName().equals("family")) @@ -694,7 +695,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { searchParamMap.add(Patient.SP_FAMILY, new StringParam("family1")); assertEquals(1, myPatientDao.search(searchParamMap).size().intValue()); - runInTransaction(()->{ + runInTransaction(() -> { Long i = myEntityManager .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamString s WHERE s.myHashIdentity = 0", Long.class) .getSingleResult(); @@ -714,7 +715,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { myResourceReindexingSvc.markAllResourcesForReindexing(); myResourceReindexingSvc.forceReindexingPass(); - runInTransaction(()->{ + runInTransaction(() -> { Long i = myEntityManager .createQuery("SELECT count(s) FROM ResourceIndexedSearchParamString s WHERE s.myHashIdentity = 0", Long.class) .getSingleResult(); @@ -808,6 +809,30 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { assertEquals("201 Created", resp.getEntry().get(0).getResponse().getStatus()); } + + @Test + public void testNestedTransaction_ReadsBlocked() { + String methodName = "testTransactionBatchWithFailingRead"; + Bundle request = new Bundle(); + request.setType(BundleType.TRANSACTION); + + Patient p = new Patient(); + p.addName().setFamily(methodName); + request.addEntry().setResource(p).getRequest().setMethod(HTTPVerb.POST); + + request.addEntry().getRequest().setMethod(HTTPVerb.GET).setUrl("Patient?identifier=foo"); + + try { + runInTransaction(()->{ + mySystemDao.transactionNested(mySrd, request); + }); + fail(); + } catch (InvalidRequestException e) { + assertEquals("Can not invoke read operation on nested transaction", e.getMessage()); + } + } + + @Test public void testTransactionBatchWithFailingRead() { String methodName = "testTransactionBatchWithFailingRead"; @@ -923,8 +948,8 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { Bundle outcome = mySystemDao.transaction(mySrd, request); ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); assertEquals("400 Bad Request", outcome.getEntry().get(0).getResponse().getStatus()); - assertEquals(IssueSeverity.ERROR, ((OperationOutcome)outcome.getEntry().get(0).getResponse().getOutcome()).getIssueFirstRep().getSeverity()); - assertEquals("Missing required resource in Bundle.entry[0].resource for operation POST", ((OperationOutcome)outcome.getEntry().get(0).getResponse().getOutcome()).getIssueFirstRep().getDiagnostics()); + assertEquals(IssueSeverity.ERROR, ((OperationOutcome) outcome.getEntry().get(0).getResponse().getOutcome()).getIssueFirstRep().getSeverity()); + assertEquals("Missing required resource in Bundle.entry[0].resource for operation POST", ((OperationOutcome) outcome.getEntry().get(0).getResponse().getOutcome()).getIssueFirstRep().getDiagnostics()); validate(outcome); } @@ -942,8 +967,8 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { Bundle outcome = mySystemDao.transaction(mySrd, request); ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); assertEquals("400 Bad Request", outcome.getEntry().get(0).getResponse().getStatus()); - assertEquals(IssueSeverity.ERROR, ((OperationOutcome)outcome.getEntry().get(0).getResponse().getOutcome()).getIssueFirstRep().getSeverity()); - assertEquals("Missing required resource in Bundle.entry[0].resource for operation PUT", ((OperationOutcome)outcome.getEntry().get(0).getResponse().getOutcome()).getIssueFirstRep().getDiagnostics()); + assertEquals(IssueSeverity.ERROR, ((OperationOutcome) outcome.getEntry().get(0).getResponse().getOutcome()).getIssueFirstRep().getSeverity()); + assertEquals("Missing required resource in Bundle.entry[0].resource for operation PUT", ((OperationOutcome) outcome.getEntry().get(0).getResponse().getOutcome()).getIssueFirstRep().getDiagnostics()); validate(outcome); } @@ -2272,7 +2297,7 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { patient2.addIdentifier().setSystem("urn:system").setValue("testPersistWithSimpleLinkP02"); request.addEntry().setResource(patient2).getRequest().setMethod(HTTPVerb.POST); - assertThrows(InvalidRequestException.class, ()->{ + assertThrows(InvalidRequestException.class, () -> { mySystemDao.transaction(mySrd, request); }); } @@ -3198,9 +3223,9 @@ public class FhirSystemDaoR4Test extends BaseJpaR4SystemTest { assertEquals("1", id2.getVersionIdPart()); assertEquals(id.getValue(), id2.getValue()); - + } - + @Test public void testTransactionWithIfMatch() { Patient p = new Patient(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java index 8d40288bc2b..b9192bf7b0f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r5/BaseJpaR5Test.java @@ -16,7 +16,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; import ca.uhn.fhir.jpa.binstore.BinaryAccessProvider; import ca.uhn.fhir.jpa.binstore.BinaryStorageInterceptor; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; import ca.uhn.fhir.jpa.config.TestR5Config; import ca.uhn.fhir.jpa.dao.BaseJpaTest; import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/ValueSetExpansionR4ElasticsearchIT.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/ValueSetExpansionR4ElasticsearchIT.java index 7680e88baec..66ac0d415cb 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/ValueSetExpansionR4ElasticsearchIT.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/ValueSetExpansionR4ElasticsearchIT.java @@ -6,7 +6,7 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc; -import ca.uhn.fhir.jpa.bulk.api.IBulkDataExportSvc; +import ca.uhn.fhir.jpa.bulk.export.api.IBulkDataExportSvc; import ca.uhn.fhir.jpa.config.TestR4ConfigWithElasticSearch; import ca.uhn.fhir.jpa.dao.BaseJpaTest; import ca.uhn.fhir.jpa.dao.data.IResourceTableDao; diff --git a/hapi-fhir-jpaserver-batch/pom.xml b/hapi-fhir-jpaserver-batch/pom.xml index b4436c319ec..a08e196fe26 100644 --- a/hapi-fhir-jpaserver-batch/pom.xml +++ b/hapi-fhir-jpaserver-batch/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-batch/src/main/java/ca/uhn/fhir/jpa/batch/BatchConstants.java b/hapi-fhir-jpaserver-batch/src/main/java/ca/uhn/fhir/jpa/batch/BatchConstants.java new file mode 100644 index 00000000000..4224e215332 --- /dev/null +++ b/hapi-fhir-jpaserver-batch/src/main/java/ca/uhn/fhir/jpa/batch/BatchConstants.java @@ -0,0 +1,32 @@ +package ca.uhn.fhir.jpa.batch; + +/*- + * #%L + * HAPI FHIR JPA Server - Batch Task Processor + * %% + * Copyright (C) 2014 - 2021 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% + */ + +public class BatchConstants { + + /** + * Non instantiable + */ + private BatchConstants() {} + + public static final String JOB_LAUNCHING_TASK_EXECUTOR = "jobLaunchingTaskExecutor"; + +} diff --git a/hapi-fhir-jpaserver-batch/src/main/java/ca/uhn/fhir/jpa/batch/config/NonPersistedBatchConfigurer.java b/hapi-fhir-jpaserver-batch/src/main/java/ca/uhn/fhir/jpa/batch/config/NonPersistedBatchConfigurer.java index 936eb9d12ab..27eb2518893 100644 --- a/hapi-fhir-jpaserver-batch/src/main/java/ca/uhn/fhir/jpa/batch/config/NonPersistedBatchConfigurer.java +++ b/hapi-fhir-jpaserver-batch/src/main/java/ca/uhn/fhir/jpa/batch/config/NonPersistedBatchConfigurer.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.batch.config; * #L% */ +import ca.uhn.fhir.jpa.batch.BatchConstants; import org.springframework.batch.core.configuration.annotation.DefaultBatchConfigurer; import org.springframework.batch.core.explore.JobExplorer; import org.springframework.batch.core.explore.support.MapJobExplorerFactoryBean; @@ -39,7 +40,7 @@ public class NonPersistedBatchConfigurer extends DefaultBatchConfigurer { private PlatformTransactionManager myHapiPlatformTransactionManager; @Autowired - @Qualifier("jobLaunchingTaskExecutor") + @Qualifier(BatchConstants.JOB_LAUNCHING_TASK_EXECUTOR) private TaskExecutor myTaskExecutor; private MapJobRepositoryFactoryBean myJobRepositoryFactory; diff --git a/hapi-fhir-jpaserver-cql/pom.xml b/hapi-fhir-jpaserver-cql/pom.xml index 440cd4e30e4..20069c8fa7e 100644 --- a/hapi-fhir-jpaserver-cql/pom.xml +++ b/hapi-fhir-jpaserver-cql/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -144,13 +144,13 @@ ca.uhn.hapi.fhir hapi-fhir-test-utilities - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT test ca.uhn.hapi.fhir hapi-fhir-jpaserver-test-utilities - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT test diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-mdm/pom.xml index 236fc33d3ab..3295a49c94a 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -55,13 +55,13 @@ ca.uhn.hapi.fhir hapi-fhir-test-utilities - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT test ca.uhn.hapi.fhir hapi-fhir-jpaserver-test-utilities - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT test diff --git a/hapi-fhir-jpaserver-migrate/pom.xml b/hapi-fhir-jpaserver-migrate/pom.xml index ab2347ac99c..68316dcf0db 100644 --- a/hapi-fhir-jpaserver-migrate/pom.xml +++ b/hapi-fhir-jpaserver-migrate/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index 4e14b8594d7..e3975350931 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.migrate.tasks; */ import ca.uhn.fhir.interceptor.model.RequestPartitionId; +import ca.uhn.fhir.jpa.entity.Search; import ca.uhn.fhir.jpa.migrate.DriverTypeEnum; import ca.uhn.fhir.jpa.migrate.taskdef.ArbitrarySqlTask; import ca.uhn.fhir.jpa.migrate.taskdef.CalculateHashesTask; @@ -91,6 +92,32 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { version.onTable("TRM_VALUESET_CONCEPT").addColumn("20210406.1", "INDEX_STATUS").nullable().type(ColumnTypeEnum.LONG); version.onTable("TRM_VALUESET_CONCEPT").addColumn("20210406.2", "SOURCE_DIRECT_PARENT_PIDS").nullable().type(ColumnTypeEnum.CLOB); version.onTable("TRM_VALUESET_CONCEPT").addColumn("20210406.3", "SOURCE_PID").nullable().type(ColumnTypeEnum.LONG); + + // Bulk Import Job + Builder.BuilderAddTableByColumns blkImportJobTable = version.addTableByColumns("20210410.1", "HFJ_BLK_IMPORT_JOB", "PID"); + blkImportJobTable.addColumn("PID").nonNullable().type(ColumnTypeEnum.LONG); + blkImportJobTable.addColumn("JOB_ID").nonNullable().type(ColumnTypeEnum.STRING, Search.UUID_COLUMN_LENGTH); + blkImportJobTable.addColumn("JOB_STATUS").nonNullable().type(ColumnTypeEnum.STRING, 10); + blkImportJobTable.addColumn("STATUS_TIME").nonNullable().type(ColumnTypeEnum.DATE_TIMESTAMP); + blkImportJobTable.addColumn("STATUS_MESSAGE").nullable().type(ColumnTypeEnum.STRING, 500); + blkImportJobTable.addColumn("OPTLOCK").nonNullable().type(ColumnTypeEnum.INT); + blkImportJobTable.addColumn("FILE_COUNT").nonNullable().type(ColumnTypeEnum.INT); + blkImportJobTable.addColumn("ROW_PROCESSING_MODE").nonNullable().type(ColumnTypeEnum.STRING, 20); + blkImportJobTable.addColumn("BATCH_SIZE").nonNullable().type(ColumnTypeEnum.INT); + blkImportJobTable.addIndex("20210410.2", "IDX_BLKIM_JOB_ID").unique(true).withColumns("JOB_ID"); + version.addIdGenerator("20210410.3", "SEQ_BLKIMJOB_PID"); + + // Bulk Import Job File + Builder.BuilderAddTableByColumns blkImportJobFileTable = version.addTableByColumns("20210410.4", "HFJ_BLK_IMPORT_JOBFILE", "PID"); + blkImportJobFileTable.addColumn("PID").nonNullable().type(ColumnTypeEnum.LONG); + blkImportJobFileTable.addColumn("JOB_PID").nonNullable().type(ColumnTypeEnum.LONG); + blkImportJobFileTable.addColumn("JOB_CONTENTS").nonNullable().type(ColumnTypeEnum.BLOB); + blkImportJobFileTable.addColumn("FILE_SEQ").nonNullable().type(ColumnTypeEnum.INT); + blkImportJobFileTable.addColumn("TENANT_NAME").nullable().type(ColumnTypeEnum.STRING, 200); + blkImportJobFileTable.addIndex("20210410.5", "IDX_BLKIM_JOBFILE_JOBID").unique(false).withColumns("JOB_PID"); + blkImportJobFileTable.addForeignKey("20210410.6", "FK_BLKIMJOBFILE_JOB").toColumn("JOB_PID").references("HFJ_BLK_IMPORT_JOB", "PID"); + version.addIdGenerator("20210410.7", "SEQ_BLKIMJOBFILE_PID"); + } private void init530() { diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index 8c915ff27c6..b46518bed86 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index 0cccf93a45b..5dc3b7d17f4 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index 633843691b3..fc862aea074 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-utilities/pom.xml b/hapi-fhir-jpaserver-test-utilities/pom.xml index e00e23537da..b66f8e9615e 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index ffbe7a1164c..183e9a6b602 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../pom.xml @@ -164,7 +164,7 @@ ca.uhn.hapi.fhir hapi-fhir-converter - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java index e2c52f3b321..a7e2c06d36a 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java @@ -6,7 +6,7 @@ import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster; import ca.uhn.fhir.jpa.api.config.DaoConfig; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; -import ca.uhn.fhir.jpa.bulk.provider.BulkDataExportProvider; +import ca.uhn.fhir.jpa.bulk.export.provider.BulkDataExportProvider; import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor; import ca.uhn.fhir.jpa.provider.DiffProvider; import ca.uhn.fhir.jpa.provider.GraphQLProvider; diff --git a/hapi-fhir-server-mdm/pom.xml b/hapi-fhir-server-mdm/pom.xml index 6f7664a8f48..adc4be9c0ab 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index 2524d2960e8..1e71fc77421 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-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 58483804fd3..d0905db3a66 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-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 bb0460f5f2c..919e895664c 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-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 ec97e7f6b0f..622de27723a 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT hapi-fhir-spring-boot-sample-client-okhttp 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 b20cdc12fac..ad0f6ed0cfb 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT hapi-fhir-spring-boot-sample-server-jersey 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 1643433e6ef..02e9d448d79 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT hapi-fhir-spring-boot-samples 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 04d4fe39150..c210826be2c 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index b3490ba702b..38c1789454d 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index d80fff6935b..c8054db0e2b 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 412d69d642c..6659bcd46c2 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGeneratorDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGeneratorDstu2Test.java index d1acfeb7369..0bb1e0e3f31 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGeneratorDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGeneratorDstu2Test.java @@ -1,7 +1,5 @@ package ca.uhn.fhir.narrative; -import ca.uhn.fhir.util.TestUtil; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -9,60 +7,54 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class BaseThymeleafNarrativeGeneratorDstu2Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseThymeleafNarrativeGeneratorDstu2Test.class); - @AfterAll - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } - - @Test public void testTrimWhitespace() { //@formatter:off - String input = "

\n" + - "
\n" + - " \n" + - " joe \n" + - " john \n" + - " BLOW \n" + - " \n" + - "
\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "
Identifier123456
Address\n" + - " \n" + - " 123 Fake Street
\n" + - " \n" + - " \n" + - " Unit 1
\n" + - " \n" + - " Toronto\n" + - " ON\n" + - " Canada\n" + - "
Date of birth\n" + - " 31 March 2014\n" + - "
\n" + - "
"; + String input = "
\n" + + "
\n" + + " \n" + + " joe \n" + + " john \n" + + " BLOW \n" + + " \n" + + "
\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
Identifier123456
Address\n" + + " \n" + + " 123 Fake Street
\n" + + " \n" + + " \n" + + " Unit 1
\n" + + " \n" + + " Toronto\n" + + " ON\n" + + " Canada\n" + + "
Date of birth\n" + + " 31 March 2014\n" + + "
\n" + + "
"; //@formatter:on String actual = BaseThymeleafNarrativeGenerator.cleanWhitespace(input); String expected = "
joe john BLOW
Identifier123456
Address123 Fake Street
Unit 1
TorontoONCanada
Date of birth31 March 2014
"; - + ourLog.info(actual); - + assertEquals(expected, actual); } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGeneratorDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGeneratorDstu2Test.java index c6709a2128b..a3742c8ea60 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGeneratorDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGeneratorDstu2Test.java @@ -1,9 +1,9 @@ package ca.uhn.fhir.narrative; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.model.dstu2.resource.Practitioner; -import ca.uhn.fhir.util.TestUtil; -import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -13,20 +13,19 @@ public class CustomThymeleafNarrativeGeneratorDstu2Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CustomThymeleafNarrativeGeneratorDstu2Test.class); - private static FhirContext ourCtx = FhirContext.forDstu2(); + private FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.DSTU2); - @AfterAll - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); + @AfterEach + public void after() { + myCtx.setNarrativeGenerator(null); } - @Test public void testGenerator() { // CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("file:src/test/resources/narrative/customnarrative.properties"); CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:narrative/customnarrative_dstu2.properties"); - ourCtx.setNarrativeGenerator(gen); + myCtx.setNarrativeGenerator(gen); Practitioner p = new Practitioner(); p.addIdentifier().setSystem("sys").setValue("val1"); @@ -34,7 +33,7 @@ public class CustomThymeleafNarrativeGeneratorDstu2Test { p.addAddress().addLine("line1").addLine("line2"); p.getName().addFamily("fam1").addGiven("given"); - gen.populateResourceNarrative(ourCtx, p); + gen.populateResourceNarrative(myCtx, p); String actual = p.getText().getDiv().getValueAsString(); ourLog.info(actual); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu2Test.java index 4b5015d9f5b..58beb7f4a0b 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu2Test.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.narrative; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.model.api.TemporalPrecisionEnum; import ca.uhn.fhir.model.dstu2.composite.CodeableConceptDt; import ca.uhn.fhir.model.dstu2.composite.QuantityDt; @@ -22,6 +23,7 @@ import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.util.TestUtil; import org.hamcrest.core.StringContains; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -35,7 +37,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class DefaultThymeleafNarrativeGeneratorDstu2Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DefaultThymeleafNarrativeGeneratorDstu2Test.class); - private static FhirContext ourCtx = FhirContext.forDstu2(); + private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.DSTU2); private DefaultThymeleafNarrativeGenerator myGen; @BeforeEach @@ -43,9 +45,15 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test { myGen = new DefaultThymeleafNarrativeGenerator(); myGen.setUseHapiServerConformanceNarrative(true); - ourCtx.setNarrativeGenerator(myGen); + myCtx.setNarrativeGenerator(myGen); } + @AfterEach + public void after() { + myCtx.setNarrativeGenerator(null); + } + + @Test public void testGeneratePatient() throws DataFormatException { Patient value = new Patient(); @@ -57,7 +65,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test { value.setBirthDate(new Date(), TemporalPrecisionEnum.DAY); - myGen.populateResourceNarrative(ourCtx, value); + myGen.populateResourceNarrative(myCtx, value); String output = value.getText().getDiv().getValueAsString(); ourLog.info(output); assertThat(output, StringContains.containsString("
joe john BLOW
")); @@ -69,7 +77,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test { Parameters value = new Parameters(); value.setId("123"); - myGen.populateResourceNarrative(ourCtx, value); + myGen.populateResourceNarrative(myCtx, value); String output = value.getText().getDiv().getValueAsString(); ourLog.info(output); assertThat(output, not(containsString("narrative"))); @@ -89,9 +97,9 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test { " \n" + ""; - OperationOutcome oo = ourCtx.newXmlParser().parseResource(OperationOutcome.class, parse); + OperationOutcome oo = myCtx.newXmlParser().parseResource(OperationOutcome.class, parse); - myGen.populateResourceNarrative(ourCtx, oo); + myGen.populateResourceNarrative(myCtx, oo); String output = oo.getText().getDiv().getValueAsString(); ourLog.info(output); @@ -129,7 +137,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test { value.addResult().setResource(obs); } - myGen.populateResourceNarrative(ourCtx, value); + myGen.populateResourceNarrative(myCtx, value); String output = value.getText().getDiv().getValueAsString(); ourLog.info(output); @@ -137,7 +145,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test { // Now try it with the parser - output = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(value); + output = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(value); ourLog.info(output); assertThat(output, StringContains.containsString("
Some & Diagnostic Report
")); } @@ -154,7 +162,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test { mp.setStatus(MedicationOrderStatusEnum.ACTIVE); mp.setDateWritten(new DateTimeDt("2014-09-01")); - myGen.populateResourceNarrative(ourCtx, mp); + myGen.populateResourceNarrative(myCtx, mp); String output = mp.getText().getDiv().getValueAsString(); assertTrue(output.contains("ciprofloaxin"), "Expected medication name of ciprofloaxin within narrative: " + output); @@ -167,7 +175,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu2Test { Medication med = new Medication(); med.getCode().setText("ciproflaxin"); - myGen.populateResourceNarrative(ourCtx, med); + myGen.populateResourceNarrative(myCtx, med); String output = med.getText().getDiv().getValueAsString(); assertThat(output, containsString("ciproflaxin")); diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index 24f6bfd53d9..04fc5a8cbc3 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java index 9d99461d525..c5dceef79ac 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.narrative; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.util.TestUtil; import org.apache.commons.collections.Transformer; @@ -11,6 +12,7 @@ import org.hl7.fhir.dstu3.model.DiagnosticReport.DiagnosticReportStatus; import org.hl7.fhir.dstu3.model.MedicationRequest.MedicationRequestStatus; import org.hl7.fhir.dstu3.model.Observation.ObservationStatus; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -28,7 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class DefaultThymeleafNarrativeGeneratorDstu3Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DefaultThymeleafNarrativeGeneratorDstu3Test.class); - private static FhirContext ourCtx = FhirContext.forDstu3(); + private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.DSTU3); private DefaultThymeleafNarrativeGenerator myGen; @BeforeEach @@ -36,9 +38,15 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test { myGen = new DefaultThymeleafNarrativeGenerator(); myGen.setUseHapiServerConformanceNarrative(true); - ourCtx.setNarrativeGenerator(myGen); + myCtx.setNarrativeGenerator(myGen); } + @AfterEach + public void after() { + myCtx.setNarrativeGenerator(null); + } + + @Test public void testGeneratePatient() throws DataFormatException { Patient value = new Patient(); @@ -51,7 +59,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test { value.setBirthDate(new Date()); - myGen.populateResourceNarrative(ourCtx, value); + myGen.populateResourceNarrative(myCtx, value); String output = value.getText().getDiv().getValueAsString(); ourLog.info(output); assertThat(output, StringContains.containsString("
joe john BLOW
")); @@ -95,7 +103,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test { } }); - customGen.populateResourceNarrative(ourCtx, value); + customGen.populateResourceNarrative(myCtx, value); String output = value.getText().getDiv().getValueAsString(); ourLog.info(output); assertThat(output, StringContains.containsString("Some beautiful proze")); @@ -111,7 +119,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test { value.addResult().setReference("Observation/2"); value.addResult().setReference("Observation/3"); - myGen.populateResourceNarrative(ourCtx, value); + myGen.populateResourceNarrative(myCtx, value); String output = value.getText().getDiv().getValueAsString(); ourLog.info(output); @@ -133,13 +141,13 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test { ""; //@formatter:on - OperationOutcome oo = ourCtx.newXmlParser().parseResource(OperationOutcome.class, parse); + OperationOutcome oo = myCtx.newXmlParser().parseResource(OperationOutcome.class, parse); // String output = gen.generateTitle(oo); // ourLog.info(output); // assertEquals("Operation Outcome (2 issues)", output); - myGen.populateResourceNarrative(ourCtx, oo); + myGen.populateResourceNarrative(myCtx, oo); String output = oo.getText().getDiv().getValueAsString(); ourLog.info(output); @@ -177,7 +185,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test { value.addResult().setResource(obs); } - myGen.populateResourceNarrative(ourCtx, value); + myGen.populateResourceNarrative(myCtx, value); String output = value.getText().getDiv().getValueAsString(); ourLog.info(output); @@ -240,8 +248,8 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test { " }"; - DiagnosticReport value = ourCtx.newJsonParser().parseResource(DiagnosticReport.class, input); - myGen.populateResourceNarrative(ourCtx, value); + DiagnosticReport value = myCtx.newJsonParser().parseResource(DiagnosticReport.class, input); + myGen.populateResourceNarrative(myCtx, value); String output = value.getText().getDiv().getValueAsString(); ourLog.info(output); @@ -261,7 +269,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test { mp.setStatus(MedicationRequestStatus.ACTIVE); mp.setAuthoredOnElement(new DateTimeType("2014-09-01")); - myGen.populateResourceNarrative(ourCtx, mp); + myGen.populateResourceNarrative(myCtx, mp); String output = mp.getText().getDiv().getValueAsString(); assertTrue(output.contains("ciprofloaxin"), "Expected medication name of ciprofloaxin within narrative: " + output); @@ -274,7 +282,7 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test { Medication med = new Medication(); med.getCode().setText("ciproflaxin"); - myGen.populateResourceNarrative(ourCtx, med); + myGen.populateResourceNarrative(myCtx, med); String output = med.getText().getDiv().getValueAsString(); assertThat(output, containsString("ciproflaxin")); diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index 035dabec631..8383bdda70a 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index 67dd6dfeb59..e0793555c76 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGeneratorR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGeneratorR4Test.java index 6b355c7332e..957551d03c5 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGeneratorR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGeneratorR4Test.java @@ -1,11 +1,13 @@ package ca.uhn.fhir.narrative; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.util.TestUtil; import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.Quantity; import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import static org.hamcrest.MatcherAssert.assertThat; @@ -14,86 +16,89 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class CustomThymeleafNarrativeGeneratorR4Test { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CustomThymeleafNarrativeGeneratorR4Test.class); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CustomThymeleafNarrativeGeneratorR4Test.class); - /** Don't use cached here since we modify the context */ - private FhirContext myCtx = FhirContext.forR4(); + /** + * Don't use cached here since we modify the context + */ + private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); - /** - * Implement narrative for standard type - */ - @Test - public void testStandardType() { + @AfterEach + public void after() { + myCtx.setNarrativeGenerator(null); + } - CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:narrative/standardtypes_r4.properties"); - myCtx.setNarrativeGenerator(gen); + /** + * Implement narrative for standard type + */ + @Test + public void testStandardType() { - Practitioner p = new Practitioner(); - p.addIdentifier().setSystem("sys").setValue("val1"); - p.addIdentifier().setSystem("sys").setValue("val2"); - p.addAddress().addLine("line1").addLine("line2"); - p.addName().setFamily("fam1").addGiven("given"); + CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:narrative/standardtypes_r4.properties"); + myCtx.setNarrativeGenerator(gen); - gen.populateResourceNarrative(myCtx, p); + Practitioner p = new Practitioner(); + p.addIdentifier().setSystem("sys").setValue("val1"); + p.addIdentifier().setSystem("sys").setValue("val2"); + p.addAddress().addLine("line1").addLine("line2"); + p.addName().setFamily("fam1").addGiven("given"); - String actual = p.getText().getDiv().getValueAsString(); - ourLog.info(actual); + gen.populateResourceNarrative(myCtx, p); - assertThat(actual, containsString("

Name

given FAM1

Address

line1
line2
")); + String actual = p.getText().getDiv().getValueAsString(); + ourLog.info(actual); - } + assertThat(actual, containsString("

Name

given FAM1

Address

line1
line2
")); - @Test - public void testCustomType() { + } - CustomPatient patient = new CustomPatient(); - patient.setActive(true); - FavouritePizzaExtension parentExtension = new FavouritePizzaExtension(); - parentExtension.setToppings(new StringType("Mushrooms, Onions")); - parentExtension.setSize(new Quantity(null, 14, "http://unitsofmeasure", "[in_i]", "Inches")); - patient.setFavouritePizza(parentExtension); + @Test + public void testCustomType() { - String output = myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient); - ourLog.info("Encoded: {}", output); + CustomPatient patient = new CustomPatient(); + patient.setActive(true); + FavouritePizzaExtension parentExtension = new FavouritePizzaExtension(); + parentExtension.setToppings(new StringType("Mushrooms, Onions")); + parentExtension.setSize(new Quantity(null, 14, "http://unitsofmeasure", "[in_i]", "Inches")); + patient.setFavouritePizza(parentExtension); - String expectedEncoding = "{\n" + - " \"resourceType\": \"Patient\",\n" + - " \"meta\": {\n" + - " \"profile\": [ \"http://custom_patient\" ]\n" + - " },\n" + - " \"extension\": [ {\n" + - " \"url\": \"http://example.com/favourite_pizza\",\n" + - " \"extension\": [ {\n" + - " \"url\": \"toppings\",\n" + - " \"valueString\": \"Mushrooms, Onions\"\n" + - " }, {\n" + - " \"url\": \"size\",\n" + - " \"valueQuantity\": {\n" + - " \"value\": 14,\n" + - " \"unit\": \"Inches\",\n" + - " \"system\": \"http://unitsofmeasure\",\n" + - " \"code\": \"[in_i]\"\n" + - " }\n" + - " } ]\n" + - " } ],\n" + - " \"active\": true\n" + - "}"; - assertEquals(expectedEncoding, output); + String output = myCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(patient); + ourLog.info("Encoded: {}", output); - CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:narrative/customtypes_r4.properties"); - myCtx.setNarrativeGenerator(gen); - gen.populateResourceNarrative(myCtx, patient); + String expectedEncoding = "{\n" + + " \"resourceType\": \"Patient\",\n" + + " \"meta\": {\n" + + " \"profile\": [ \"http://custom_patient\" ]\n" + + " },\n" + + " \"extension\": [ {\n" + + " \"url\": \"http://example.com/favourite_pizza\",\n" + + " \"extension\": [ {\n" + + " \"url\": \"toppings\",\n" + + " \"valueString\": \"Mushrooms, Onions\"\n" + + " }, {\n" + + " \"url\": \"size\",\n" + + " \"valueQuantity\": {\n" + + " \"value\": 14,\n" + + " \"unit\": \"Inches\",\n" + + " \"system\": \"http://unitsofmeasure\",\n" + + " \"code\": \"[in_i]\"\n" + + " }\n" + + " } ]\n" + + " } ],\n" + + " \"active\": true\n" + + "}"; + assertEquals(expectedEncoding, output); - String actual = patient.getText().getDiv().getValueAsString(); - ourLog.info(actual); + CustomThymeleafNarrativeGenerator gen = new CustomThymeleafNarrativeGenerator("classpath:narrative/customtypes_r4.properties"); + myCtx.setNarrativeGenerator(gen); + gen.populateResourceNarrative(myCtx, patient); - String expected = "

CustomPatient

Favourite Pizza

Toppings: Mushrooms, Onions Size: 14
"; - assertEquals(expected, actual); + String actual = patient.getText().getDiv().getValueAsString(); + ourLog.info(actual); - } + String expected = "

CustomPatient

Favourite Pizza

Toppings: Mushrooms, Onions Size: 14
"; + assertEquals(expected, actual); + + } - @AfterAll - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorR4Test.java index 433b448a448..4847d922123 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorR4Test.java @@ -10,6 +10,7 @@ import org.hl7.fhir.r4.model.DiagnosticReport.DiagnosticReportStatus; import org.hl7.fhir.r4.model.MedicationRequest.MedicationRequestStatus; import org.hl7.fhir.r4.model.Observation.ObservationStatus; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -22,7 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class DefaultThymeleafNarrativeGeneratorR4Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DefaultThymeleafNarrativeGeneratorR4Test.class); - private FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); + private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); private DefaultThymeleafNarrativeGenerator myGen; @BeforeEach @@ -33,6 +34,11 @@ public class DefaultThymeleafNarrativeGeneratorR4Test { myCtx.setNarrativeGenerator(myGen); } + @AfterEach + public void after() { + myCtx.setNarrativeGenerator(null); + } + @Test public void testGeneratePatient() throws DataFormatException { Patient value = new Patient(); diff --git a/hapi-fhir-structures-r5/pom.xml b/hapi-fhir-structures-r5/pom.xml index 8760ce684e3..f564ed044b8 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index 654c33c28fe..ac4521d10e5 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java index ee503727c3e..61563e7ccb4 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java @@ -154,7 +154,7 @@ public interface ITestDataBuilder { } } - default IBaseResource buildResource(String theResourceType, Consumer[] theModifiers) { + default IBaseResource buildResource(String theResourceType, Consumer... theModifiers) { IBaseResource resource = getFhirContext().getResourceDefinition(theResourceType).newInstance(); for (Consumer next : theModifiers) { next.accept(resource); diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 1dfb066f20b..0a140bc05ab 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml index 6aae3187f80..4db0f72591a 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-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 ecdbbd4a635..a3b7462547d 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-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 560106316d0..33d67b4e94a 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-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 68ae7002bdd..5f830ffce69 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-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 9ac87f4b79d..dbc5e9359b8 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index b180c3de905..aafc58b15ba 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index 5e946ea683b..6daf487888f 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../pom.xml @@ -58,37 +58,37 @@ ca.uhn.hapi.fhir hapi-fhir-structures-dstu3 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-hl7org-dstu2 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-r4 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-r5 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu2 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu3 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-r4 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT org.apache.velocity diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index b4a180ec8ca..6b9aa6546e9 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index d53c158c6e0..723188cb297 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir pom - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT HAPI-FHIR An open-source implementation of the FHIR specification in Java. https://hapifhir.io @@ -761,20 +761,21 @@ 1.2.0 4.2.5 1.2 - 2.7.0 - 1.14 + 3.0.1 + 1.15 1.20 - 1.7 - 2.6 - 3.9 + 1.9 + 2.8.0 + 3.12.0 1.2 1.5.0 10.14.2.0 2.5.1 + 3.9.0 0.7.9 - 30.1-jre - 2.8.5 + 30.1.1-jre + 2.8.6 2.2.11_1 2.3.1 2.3.0.1 @@ -786,17 +787,17 @@ 3.0.2 5.7.0 6.5.4 - 5.4.26.Final - 6.0.0.Final + 5.4.30.Final + 6.0.2.Final 8.7.0 2.2 6.1.5.Final 4.4.13 4.5.13 - 2.12.1 - 2.11.3 - 3.1.0 + 2.12.3 + ${jackson_version} + 3.3.0 1.8 3.8.1 4.0.0.Beta3 @@ -807,15 +808,15 @@ 1.2_5 1.7.30 2.11.1 - 5.3.3 + 5.3.6 - 2.4.2 - 4.2.3.RELEASE + 2.4.7 + 4.3.2 2.4.1 1.2.2.RELEASE 3.1.4 - 3.0.11.RELEASE + 3.0.12.RELEASE 4.4.1 @@ -999,7 +1000,7 @@ org.jetbrains annotations - 19.0.0 + 20.1.0 commons-io @@ -1150,7 +1151,7 @@ org.apache.commons commons-dbcp2 - 2.7.0 + 2.8.0 org.apache.commons @@ -1312,7 +1313,7 @@ com.fasterxml.woodstox woodstox-core - 6.2.3 + 6.2.5 org.ebaysf.web @@ -1398,7 +1399,7 @@ org.fusesource.jansi jansi - 2.1.1 + 2.3.2 org.glassfish @@ -1553,12 +1554,12 @@ org.mockito mockito-core - 3.6.28 + ${mockito_version} org.mockito mockito-junit-jupiter - 3.3.3 + ${mockito_version} org.postgresql @@ -1817,18 +1818,10 @@ true - - com.gemnasium - gemnasium-maven-plugin - 0.2.0 - - github.com/hapifhir/hapi-fhir - - org.basepom.maven duplicate-finder-maven-plugin - 1.4.0 + 1.5.0 de.jpdigital @@ -1889,12 +1882,12 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.1.1 + 3.2.0 org.apache.maven.plugins maven-jar-plugin - 3.1.2 + 3.2.0 org.apache.maven.plugins @@ -1909,7 +1902,7 @@ org.apache.maven.plugins maven-plugin-plugin - 3.5 + 3.6.0 org.apache.maven.plugins @@ -1919,14 +1912,7 @@ org.apache.maven.plugins maven-source-plugin - 3.1.0 - - - org.codehaus.plexus - plexus-utils - 3.1.0 - - + 3.2.1 org.apache.maven.plugins @@ -1948,7 +1934,7 @@ org.codehaus.mojo build-helper-maven-plugin - 3.0.0 + 3.2.0 org.codehaus.mojo @@ -1981,7 +1967,7 @@ org.codehaus.mojo versions-maven-plugin - 2.7 + 2.8.1 false @@ -2110,7 +2096,7 @@ org.apache.maven.plugins maven-checkstyle-plugin - 3.1.0 + 3.1.2 com.puppycrawl.tools @@ -2143,7 +2129,7 @@ - 3.3.9 + 3.5.4 11 diff --git a/restful-server-example/pom.xml b/restful-server-example/pom.xml index 37c4a874033..7640191ae97 100644 --- a/restful-server-example/pom.xml +++ b/restful-server-example/pom.xml @@ -8,7 +8,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../pom.xml diff --git a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml index 4e51e91c572..2ef7b674d4b 100644 --- a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml +++ b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-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 20a96ef3c9e..85242f389d3 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-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 897bcbb0b07..9716634f826 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 - 5.4.0-PRE5-SNAPSHOT + 5.4.0-PRE6-SNAPSHOT ../../pom.xml From b294f7d20837b6649eeb50959f4e38a6c2f27bfd Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Wed, 14 Apr 2021 18:00:52 -0400 Subject: [PATCH 12/39] License headers --- .../fhir/context/phonetic/NumericEncoder.java | 20 +++++++++++++++++++ .../mdm/rules/matcher/NumericMatcher.java | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/NumericEncoder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/NumericEncoder.java index 1619748d470..f293977b318 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/NumericEncoder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/phonetic/NumericEncoder.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.context.phonetic; +/*- + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2021 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 com.google.common.base.CharMatcher; // Useful for numerical identifiers like phone numbers, address parts etc. diff --git a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NumericMatcher.java b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NumericMatcher.java index 82bce7d59c0..92a2c558e4b 100644 --- a/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NumericMatcher.java +++ b/hapi-fhir-server-mdm/src/main/java/ca/uhn/fhir/mdm/rules/matcher/NumericMatcher.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.mdm.rules.matcher; +/*- + * #%L + * HAPI FHIR - Master Data Management + * %% + * Copyright (C) 2014 - 2021 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.phonetic.NumericEncoder; // Useful for numerical identifiers like phone numbers, address parts etc. From 6b43410514622b923fc368409bae5aaea59ae110 Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Wed, 14 Apr 2021 18:50:47 -0400 Subject: [PATCH 13/39] Add missing constant --- .../src/main/java/ca/uhn/fhir/util/VersionEnum.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java index 8af93635c43..773cc30bd26 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java @@ -68,7 +68,9 @@ public enum VersionEnum { V5_2_0, V5_2_1, V5_3_0, - V5_4_0; + V5_3_2, + V5_4_0, + ; public static VersionEnum latestVersion() { VersionEnum[] values = VersionEnum.values(); From 120605ebababb956890dd1d9d46c24c2438b91de Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Wed, 14 Apr 2021 19:56:25 -0400 Subject: [PATCH 14/39] Add to task --- .../ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java | 1 + 1 file changed, 1 insertion(+) diff --git a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java index e3975350931..13bb76eb723 100644 --- a/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java +++ b/hapi-fhir-jpaserver-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/tasks/HapiFhirJpaMigrationTasks.java @@ -100,6 +100,7 @@ public class HapiFhirJpaMigrationTasks extends BaseMigrationTasks { blkImportJobTable.addColumn("JOB_STATUS").nonNullable().type(ColumnTypeEnum.STRING, 10); blkImportJobTable.addColumn("STATUS_TIME").nonNullable().type(ColumnTypeEnum.DATE_TIMESTAMP); blkImportJobTable.addColumn("STATUS_MESSAGE").nullable().type(ColumnTypeEnum.STRING, 500); + blkImportJobTable.addColumn("JOB_DESC").nullable().type(ColumnTypeEnum.STRING, 500); blkImportJobTable.addColumn("OPTLOCK").nonNullable().type(ColumnTypeEnum.INT); blkImportJobTable.addColumn("FILE_COUNT").nonNullable().type(ColumnTypeEnum.INT); blkImportJobTable.addColumn("ROW_PROCESSING_MODE").nonNullable().type(ColumnTypeEnum.STRING, 20); From 9a67f3ee0cffe22ae718619ec187e3f23ba12393 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Apr 2021 20:09:37 -0400 Subject: [PATCH 15/39] Bump spring_boot_version from 2.4.1 to 2.4.4 (#2550) Bumps `spring_boot_version` from 2.4.1 to 2.4.4. Updates `spring-boot-starter-test` from 2.4.1 to 2.4.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.4.1...v2.4.4) Updates `spring-boot-test` from 2.4.1 to 2.4.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.4.1...v2.4.4) Updates `spring-boot-maven-plugin` from 2.4.1 to 2.4.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.4.1...v2.4.4) Updates `spring-boot-dependencies` from 2.4.1 to 2.4.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.4.1...v2.4.4) Updates `spring-boot-autoconfigure` from 2.4.1 to 2.4.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.4.1...v2.4.4) Updates `spring-boot-configuration-processor` from 2.4.1 to 2.4.4 - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v2.4.1...v2.4.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 723188cb297..1a7e01bbe03 100644 --- a/pom.xml +++ b/pom.xml @@ -812,7 +812,7 @@ 2.4.7 4.3.2 - 2.4.1 + 2.4.4 1.2.2.RELEASE 3.1.4 From 05b1323638dac3d1e53657ffbeaff3cff28231b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Apr 2021 08:21:33 -0400 Subject: [PATCH 16/39] Bump jarchivelib from 1.0.0 to 1.1.0 (#2553) Bumps [jarchivelib](https://github.com/thrau/jarchivelib) from 1.0.0 to 1.1.0. - [Release notes](https://github.com/thrau/jarchivelib/releases) - [Commits](https://github.com/thrau/jarchivelib/compare/v1.0.0...v1.1.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 1a7e01bbe03..d1f94242138 100644 --- a/pom.xml +++ b/pom.xml @@ -1393,7 +1393,7 @@ org.rauschig jarchivelib - 1.0.0 + 1.1.0 test From 42c89ccedd15bc8757bf4d3e72c0077f5c5936db Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Thu, 15 Apr 2021 08:22:44 -0400 Subject: [PATCH 17/39] Changelog --- .../main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml index c973340bfd7..928093b5dd3 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml @@ -21,5 +21,6 @@
  • Commons DBCP2 (JPA): 2.7.0 -> 2.8.0
  • Thymeleaf (Testpage Overlay): 3.0.11.RELEASE -> 3.0.12.RELEASE
  • JAnsi (CLI): 2.1.1 -> 2.3.2
  • +
  • JArchivelib (CLI): 1.0.0 -> 1.1.0
  • " From d94611edf62d17b17608c1b6f6073c29634d4a41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 15 Apr 2021 08:23:30 -0400 Subject: [PATCH 18/39] Bump junit_version from 5.7.0 to 5.7.1 (#2554) Bumps `junit_version` from 5.7.0 to 5.7.1. Updates `junit-jupiter` from 5.7.0 to 5.7.1 - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.7.0...r5.7.1) Updates `junit-jupiter-api` from 5.7.0 to 5.7.1 - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.7.0...r5.7.1) Updates `junit-jupiter-engine` from 5.7.0 to 5.7.1 - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.7.0...r5.7.1) Updates `junit-jupiter-params` from 5.7.0 to 5.7.1 - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.7.0...r5.7.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d1f94242138..a30317cecab 100644 --- a/pom.xml +++ b/pom.xml @@ -785,7 +785,7 @@ 9.4.39.v20210325 3.0.2 - 5.7.0 + 5.7.1 6.5.4 5.4.30.Final 6.0.2.Final From 7cabcbd772a604317a19e73d40c3cfe7c106ac42 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Fri, 16 Apr 2021 11:12:40 -0400 Subject: [PATCH 19/39] Fix partition selection for system request details --- .../bulk/export/job/GroupBulkItemReader.java | 12 ++-- .../bulk/export/job/ResourceToFileWriter.java | 3 +- .../partition/RequestPartitionHelperSvc.java | 30 ++++++++- .../jpa/bulk/BulkDataExportSvcImplR4Test.java | 61 ++++++++++++++++--- .../uhn/fhir/jpa/model/util/JpaConstants.java | 5 ++ 5 files changed, 95 insertions(+), 16 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java index 3a10fec2aae..acac73df135 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java @@ -29,6 +29,8 @@ import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.mdm.MdmExpansionCacheSvc; import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails; +import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.jpa.partition.SystemRequestDetails; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.util.QueryChunker; import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; @@ -178,13 +180,13 @@ public class GroupBulkItemReader extends BaseBulkItemReader implements ItemReade * @return A list of strings representing the Patient IDs of the members (e.g. ["P1", "P2", "P3"] */ private List getMembers() { - IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId)); + SystemRequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setTenantId(JpaConstants.ALL_PARTITIONS_NAME); + IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId), requestDetails); List evaluate = myContext.newFhirPath().evaluate(group, "member.entity.reference", IPrimitiveType.class); return evaluate.stream().map(IPrimitiveType::getValueAsString).collect(Collectors.toList()); } - - /** * Given the local myGroupId, perform an expansion to retrieve all resource IDs of member patients. * if myMdmEnabled is set to true, we also reach out to the IMdmLinkDao to attempt to also expand it into matched @@ -194,7 +196,9 @@ public class GroupBulkItemReader extends BaseBulkItemReader implements ItemReade */ private Set expandAllPatientPidsFromGroup() { Set expandedIds = new HashSet<>(); - IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId)); + SystemRequestDetails requestDetails = new SystemRequestDetails(); + requestDetails.setTenantId(JpaConstants.ALL_PARTITIONS_NAME); + IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId), new SystemRequestDetails()); Long pidOrNull = myIdHelperService.getPidOrNull(group); //Attempt to perform MDM Expansion of membership diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/ResourceToFileWriter.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/ResourceToFileWriter.java index 8b4ebe7e86a..df362a79f19 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/ResourceToFileWriter.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/ResourceToFileWriter.java @@ -27,6 +27,7 @@ import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.batch.log.Logs; import ca.uhn.fhir.jpa.bulk.export.svc.BulkExportDaoSvc; import ca.uhn.fhir.jpa.entity.BulkExportCollectionFileEntity; +import ca.uhn.fhir.jpa.partition.SystemRequestDetails; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.util.BinaryUtil; @@ -100,7 +101,7 @@ public class ResourceToFileWriter implements ItemWriter> { IBaseBinary binary = BinaryUtil.newBinary(myFhirContext); binary.setContentType(Constants.CT_FHIR_NDJSON); binary.setContent(myOutputStream.toByteArray()); - DaoMethodOutcome outcome = myBinaryDao.create(binary); + DaoMethodOutcome outcome = myBinaryDao.create(binary, new SystemRequestDetails()); return outcome.getResource().getIdElement(); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java index 37da051117a..94d360f1b31 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java @@ -44,6 +44,7 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; +import static ca.uhn.fhir.jpa.model.util.JpaConstants.ALL_PARTITIONS_NAME; import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.doCallHooks; import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.doCallHooksAndReturnObject; import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.hasHooks; @@ -101,6 +102,19 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { return RequestPartitionId.defaultPartition(); } + //Shortcircuit and write system calls out to default partition. + if (theRequest instanceof SystemRequestDetails) { + if (theRequest.getTenantId() != null) { + if (theRequest.getTenantId().equals(ALL_PARTITIONS_NAME)) { + return RequestPartitionId.allPartitions(); + } else { + return RequestPartitionId.fromPartitionName(theRequest.getTenantId()); + } + } else { + return RequestPartitionId.defaultPartition(); + } + } + // Interceptor call: STORAGE_PARTITION_IDENTIFY_READ if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_READ, myInterceptorBroadcaster, theRequest)) { HookParams params = new HookParams() @@ -129,7 +143,21 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { if (myPartitionSettings.isPartitioningEnabled()) { - // Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE + //Shortcircuit and write system calls out to default partition. + if (theRequest instanceof SystemRequestDetails) { + if (theRequest.getTenantId() != null) { + if (theRequest.getTenantId().equals(ALL_PARTITIONS_NAME)) { + return RequestPartitionId.allPartitions(); + } else { + return RequestPartitionId.fromPartitionName(theRequest.getTenantId()); + } + } else { + return RequestPartitionId.defaultPartition(); + } + } + + + // Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE HookParams params = new HookParams() .add(IBaseResource.class, theResource) .add(RequestDetails.class, theRequest) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java index fdc92d090e9..902e5019e98 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java @@ -18,6 +18,7 @@ import ca.uhn.fhir.jpa.entity.BulkExportCollectionEntity; import ca.uhn.fhir.jpa.entity.BulkExportCollectionFileEntity; import ca.uhn.fhir.jpa.entity.BulkExportJobEntity; import ca.uhn.fhir.jpa.entity.MdmLink; +import ca.uhn.fhir.jpa.partition.SystemRequestDetails; import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; import ca.uhn.fhir.parser.IParser; @@ -494,7 +495,7 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { Patient patient = new Patient(); patient.setId("PAT" + i); patient.addIdentifier().setSystem("http://mrns").setValue("PAT" + i); - myPatientDao.update(patient).getId().toUnqualifiedVersionless(); + myPatientDao.update(patient, new SystemRequestDetails()).getId().toUnqualifiedVersionless(); } // Create a bulk job @@ -848,7 +849,7 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { public String getBinaryContents(IBulkDataExportSvc.JobInfo theJobInfo, int theIndex) { // Iterate over the files - Binary nextBinary = myBinaryDao.read(theJobInfo.getFiles().get(theIndex).getResourceId()); + Binary nextBinary = myBinaryDao.read(theJobInfo.getFiles().get(theIndex).getResourceId(), new SystemRequestDetails()); assertEquals(Constants.CT_FHIR_NDJSON, nextBinary.getContentType()); String nextContents = new String(nextBinary.getContent(), Constants.CHARSET_UTF8); ourLog.info("Next contents for type {}:\n{}", nextBinary.getResourceType(), nextContents); @@ -928,7 +929,7 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { //Check Observation Content - Binary observationExportContent = myBinaryDao.read(jobInfo.getFiles().get(1).getResourceId()); + Binary observationExportContent = myBinaryDao.read(jobInfo.getFiles().get(1).getResourceId(), new SystemRequestDetails()); assertEquals(Constants.CT_FHIR_NDJSON, observationExportContent.getContentType()); nextContents = new String(observationExportContent.getContent(), Constants.CHARSET_UTF8); ourLog.info("Next contents for type {}:\n{}", observationExportContent.getResourceType(), nextContents); @@ -1061,7 +1062,47 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { //Now if we create another one and ask for the cache, we should get the most-recently-insert entry. IBulkDataExportSvc.JobInfo jobInfo10 = myBulkDataExportSvc.submitJob(options, true); assertThat(jobInfo10.getJobId(), is(equalTo(jobInfo9.getJobId()))); + } + @Test + public void testBulkExportWritesToDEFAULTPartitionWhenPartitioningIsEnabled() { + myPartitionSettings.setPartitioningEnabled(true); + createResources(); + + //Only get COVID-19 vaccinations + Set filters = new HashSet<>(); + filters.add("Immunization?vaccine-code=vaccines|COVID-19"); + + BulkDataExportOptions bulkDataExportOptions = new BulkDataExportOptions(); + bulkDataExportOptions.setOutputFormat(null); + bulkDataExportOptions.setResourceTypes(Sets.newHashSet("Immunization")); + bulkDataExportOptions.setSince(null); + bulkDataExportOptions.setFilters(filters); + bulkDataExportOptions.setGroupId(myPatientGroupId); + bulkDataExportOptions.setExpandMdm(true); + bulkDataExportOptions.setExportStyle(BulkDataExportOptions.ExportStyle.GROUP); + IBulkDataExportSvc.JobInfo jobDetails = myBulkDataExportSvc.submitJob(bulkDataExportOptions); + + myBulkDataExportSvc.buildExportFiles(); + awaitAllBulkJobCompletions(); + + IBulkDataExportSvc.JobInfo jobInfo = myBulkDataExportSvc.getJobInfoOrThrowResourceNotFound(jobDetails.getJobId()); + + assertThat(jobInfo.getStatus(), equalTo(BulkExportJobStatusEnum.COMPLETE)); + assertThat(jobInfo.getFiles().size(), equalTo(1)); + assertThat(jobInfo.getFiles().get(0).getResourceType(), is(equalTo("Immunization"))); + + // Check immunization Content + String nextContents = getBinaryContents(jobInfo, 0); + + assertThat(nextContents, is(containsString("IMM1"))); + assertThat(nextContents, is(containsString("IMM3"))); + assertThat(nextContents, is(containsString("IMM5"))); + assertThat(nextContents, is(containsString("IMM7"))); + assertThat(nextContents, is(containsString("IMM9"))); + assertThat(nextContents, is(containsString("IMM999"))); + + assertThat(nextContents, is(not(containsString("Flu")))); } private void createResources() { @@ -1071,7 +1112,7 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { //Manually create a golden record Patient goldenPatient = new Patient(); goldenPatient.setId("PAT999"); - DaoMethodOutcome g1Outcome = myPatientDao.update(goldenPatient); + DaoMethodOutcome g1Outcome = myPatientDao.update(goldenPatient, new SystemRequestDetails()); Long goldenPid = myIdHelperService.getPidOrNull(g1Outcome.getResource()); //Create our golden records' data. @@ -1098,12 +1139,12 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { createCareTeamWithIndex(i, patId); } - myPatientGroupId = myGroupDao.update(group).getId(); + myPatientGroupId = myGroupDao.update(group, new SystemRequestDetails()).getId(); //Manually create another golden record Patient goldenPatient2 = new Patient(); goldenPatient2.setId("PAT888"); - DaoMethodOutcome g2Outcome = myPatientDao.update(goldenPatient2); + DaoMethodOutcome g2Outcome = myPatientDao.update(goldenPatient2, new SystemRequestDetails()); Long goldenPid2 = myIdHelperService.getPidOrNull(g2Outcome.getResource()); //Create some nongroup patients MDM linked to a different golden resource. They shouldnt be included in the query. @@ -1132,14 +1173,14 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { patient.setGender(i % 2 == 0 ? Enumerations.AdministrativeGender.MALE : Enumerations.AdministrativeGender.FEMALE); patient.addName().setFamily("FAM" + i); patient.addIdentifier().setSystem("http://mrns").setValue("PAT" + i); - return myPatientDao.update(patient); + return myPatientDao.update(patient, new SystemRequestDetails()); } private void createCareTeamWithIndex(int i, IIdType patId) { CareTeam careTeam = new CareTeam(); careTeam.setId("CT" + i); careTeam.setSubject(new Reference(patId)); // This maps to the "patient" search parameter on CareTeam - myCareTeamDao.update(careTeam); + myCareTeamDao.update(careTeam, new SystemRequestDetails()); } private void createImmunizationWithIndex(int i, IIdType patId) { @@ -1157,7 +1198,7 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { cc.addCoding().setSystem("vaccines").setCode("COVID-19"); immunization.setVaccineCode(cc); } - myImmunizationDao.update(immunization); + myImmunizationDao.update(immunization, new SystemRequestDetails()); } private void createObservationWithIndex(int i, IIdType patId) { @@ -1168,7 +1209,7 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { if (patId != null) { obs.getSubject().setReference(patId.getValue()); } - myObservationDao.update(obs); + myObservationDao.update(obs, new SystemRequestDetails()); } public void linkToGoldenResource(Long theGoldenPid, Long theSourcePid) { diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java index feb1fbf5bd2..2d7c10fa796 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/util/JpaConstants.java @@ -218,6 +218,11 @@ public class JpaConstants { */ public static final String DEFAULT_PARTITION_NAME = "DEFAULT"; + /** + * The name of the collection of all partitions + */ + public static final String ALL_PARTITIONS_NAME = "ALL_PARTITIONS"; + /** * Parameter for the $expand operation */ From efe5b7b14055c7c6335492508d5d1f8ff64fdc3c Mon Sep 17 00:00:00 2001 From: Tadgh Date: Fri, 16 Apr 2021 11:24:42 -0400 Subject: [PATCH 20/39] Fix package cache usage --- .../ca/uhn/fhir/jpa/packages/JpaPackageCache.java | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java index 6c64b331a17..76f621729cd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/JpaPackageCache.java @@ -65,7 +65,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.support.TransactionTemplate; @@ -655,16 +654,8 @@ public class JpaPackageCache extends BasePackageCacheManager implements IHapiPac } private void deleteAndExpungeResourceBinary(IIdType theResourceBinaryId, ExpungeOptions theOptions) { - - if (myPartitionSettings.isPartitioningEnabled()) { - SystemRequestDetails requestDetails = new SystemRequestDetails(); - requestDetails.setTenantId(JpaConstants.DEFAULT_PARTITION_NAME); - getBinaryDao().delete(theResourceBinaryId, requestDetails).getEntity(); - getBinaryDao().forceExpungeInExistingTransaction(theResourceBinaryId, theOptions, requestDetails); - } else { - getBinaryDao().delete(theResourceBinaryId).getEntity(); - getBinaryDao().forceExpungeInExistingTransaction(theResourceBinaryId, theOptions, null); - } + getBinaryDao().delete(theResourceBinaryId, new SystemRequestDetails()).getEntity(); + getBinaryDao().forceExpungeInExistingTransaction(theResourceBinaryId, theOptions, new SystemRequestDetails()); } From 8035d51e48ad4fdde97c8315a037d9dc1912d261 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Fri, 16 Apr 2021 11:31:43 -0400 Subject: [PATCH 21/39] Refactor, comment --- .../partition/RequestPartitionHelperSvc.java | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java index 94d360f1b31..ceecbb9458d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java @@ -35,6 +35,7 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.Nonnull; @@ -104,15 +105,7 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { //Shortcircuit and write system calls out to default partition. if (theRequest instanceof SystemRequestDetails) { - if (theRequest.getTenantId() != null) { - if (theRequest.getTenantId().equals(ALL_PARTITIONS_NAME)) { - return RequestPartitionId.allPartitions(); - } else { - return RequestPartitionId.fromPartitionName(theRequest.getTenantId()); - } - } else { - return RequestPartitionId.defaultPartition(); - } + return getSystemRequestPartitionId(theRequest); } // Interceptor call: STORAGE_PARTITION_IDENTIFY_READ @@ -133,6 +126,29 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { return RequestPartitionId.allPartitions(); } + /** + * Determine the partition for a System Call (defined by the fact that the request is of type SystemRequestDetails) + * + * 1. If the tenant ID is set to the constant for all partitions, return all partitions + * 2. If there is a tenant ID set in the request, use it. + * 3. Otherwise, return the Default Partition. + * + * @param theRequest The {@link SystemRequestDetails} + * @return the {@link RequestPartitionId} to be used for this request. + */ + @NotNull + private RequestPartitionId getSystemRequestPartitionId(@NotNull RequestDetails theRequest) { + if (theRequest.getTenantId() != null) { + if (theRequest.getTenantId().equals(ALL_PARTITIONS_NAME)) { + return RequestPartitionId.allPartitions(); + } else { + return RequestPartitionId.fromPartitionName(theRequest.getTenantId()); + } + } else { + return RequestPartitionId.defaultPartition(); + } + } + /** * Invoke the {@link Pointcut#STORAGE_PARTITION_IDENTIFY_CREATE} interceptor pointcut to determine the tenant for a create request. */ @@ -145,15 +161,7 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { //Shortcircuit and write system calls out to default partition. if (theRequest instanceof SystemRequestDetails) { - if (theRequest.getTenantId() != null) { - if (theRequest.getTenantId().equals(ALL_PARTITIONS_NAME)) { - return RequestPartitionId.allPartitions(); - } else { - return RequestPartitionId.fromPartitionName(theRequest.getTenantId()); - } - } else { - return RequestPartitionId.defaultPartition(); - } + return getSystemRequestPartitionId(theRequest); } From fad32aa636208152d641211422c921b6e448a185 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Fri, 16 Apr 2021 11:55:12 -0400 Subject: [PATCH 22/39] Partition management for expired jobs --- .../fhir/jpa/bulk/export/job/GroupBulkItemReader.java | 11 +++++++---- .../jpa/bulk/export/svc/BulkDataExportSvcImpl.java | 5 +++-- .../fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java | 7 ++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java index acac73df135..ca6e3cbe699 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java @@ -29,7 +29,6 @@ import ca.uhn.fhir.jpa.dao.data.IMdmLinkDao; import ca.uhn.fhir.jpa.dao.index.IdHelperService; import ca.uhn.fhir.jpa.dao.mdm.MdmExpansionCacheSvc; import ca.uhn.fhir.jpa.model.search.SearchRuntimeDetails; -import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.partition.SystemRequestDetails; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.util.QueryChunker; @@ -56,6 +55,8 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import static ca.uhn.fhir.jpa.model.util.JpaConstants.ALL_PARTITIONS_NAME; + /** * Bulk Item reader for the Group Bulk Export job. * Instead of performing a normal query on the resource type using type filters, we instead @@ -120,7 +121,9 @@ public class GroupBulkItemReader extends BaseBulkItemReader implements ItemReade Set patientPidsToExport = new HashSet<>(pidsOrThrowException); if (myMdmEnabled) { - IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId)); + SystemRequestDetails srd = new SystemRequestDetails(); + srd.setTenantId(ALL_PARTITIONS_NAME); + IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId), srd); Long pidOrNull = myIdHelperService.getPidOrNull(group); List goldenPidSourcePidTuple = myMdmLinkDao.expandPidsFromGroupPidGivenMatchResult(pidOrNull, MdmMatchResultEnum.MATCH); goldenPidSourcePidTuple.forEach(tuple -> { @@ -181,7 +184,7 @@ public class GroupBulkItemReader extends BaseBulkItemReader implements ItemReade */ private List getMembers() { SystemRequestDetails requestDetails = new SystemRequestDetails(); - requestDetails.setTenantId(JpaConstants.ALL_PARTITIONS_NAME); + requestDetails.setTenantId(ALL_PARTITIONS_NAME); IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId), requestDetails); List evaluate = myContext.newFhirPath().evaluate(group, "member.entity.reference", IPrimitiveType.class); return evaluate.stream().map(IPrimitiveType::getValueAsString).collect(Collectors.toList()); @@ -197,7 +200,7 @@ public class GroupBulkItemReader extends BaseBulkItemReader implements ItemReade private Set expandAllPatientPidsFromGroup() { Set expandedIds = new HashSet<>(); SystemRequestDetails requestDetails = new SystemRequestDetails(); - requestDetails.setTenantId(JpaConstants.ALL_PARTITIONS_NAME); + requestDetails.setTenantId(ALL_PARTITIONS_NAME); IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId), new SystemRequestDetails()); Long pidOrNull = myIdHelperService.getPidOrNull(group); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkDataExportSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkDataExportSvcImpl.java index 872c036f63a..1dc8d672af7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkDataExportSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/svc/BulkDataExportSvcImpl.java @@ -42,6 +42,7 @@ import ca.uhn.fhir.jpa.model.sched.HapiJob; import ca.uhn.fhir.jpa.model.sched.ISchedulerService; import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition; import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.jpa.partition.SystemRequestDetails; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; @@ -203,8 +204,8 @@ public class BulkDataExportSvcImpl implements IBulkDataExportSvc { for (BulkExportCollectionFileEntity nextFile : nextCollection.getFiles()) { ourLog.info("Purging bulk data file: {}", nextFile.getResourceId()); - getBinaryDao().delete(toId(nextFile.getResourceId())); - getBinaryDao().forceExpungeInExistingTransaction(toId(nextFile.getResourceId()), new ExpungeOptions().setExpungeDeletedResources(true).setExpungeOldVersions(true), null); + getBinaryDao().delete(toId(nextFile.getResourceId()), new SystemRequestDetails()); + getBinaryDao().forceExpungeInExistingTransaction(toId(nextFile.getResourceId()), new ExpungeOptions().setExpungeDeletedResources(true).setExpungeOldVersions(true), new SystemRequestDetails()); myBulkExportCollectionFileDao.deleteByPid(nextFile.getId()); } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java index 902e5019e98..432bab10282 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java @@ -117,7 +117,7 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { Binary b = new Binary(); b.setContent(new byte[]{0, 1, 2, 3}); - String binaryId = myBinaryDao.create(b).getId().toUnqualifiedVersionless().getValue(); + String binaryId = myBinaryDao.create(b, new SystemRequestDetails()).getId().toUnqualifiedVersionless().getValue(); BulkExportJobEntity job = new BulkExportJobEntity(); job.setStatus(BulkExportJobStatusEnum.COMPLETE); @@ -524,7 +524,7 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { // Iterate over the files for (IBulkDataExportSvc.FileEntry next : status.getFiles()) { - Binary nextBinary = myBinaryDao.read(next.getResourceId()); + Binary nextBinary = myBinaryDao.read(next.getResourceId(), new SystemRequestDetails()); assertEquals(Constants.CT_FHIR_NDJSON, nextBinary.getContentType()); String nextContents = new String(nextBinary.getContent(), Constants.CHARSET_UTF8); ourLog.info("Next contents for type {}:\n{}", next.getResourceType(), nextContents); @@ -1030,7 +1030,7 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { } @Test - public void testCacheSettingIsRespectedWhenCreatingNewJobs() { + public void testCacheSettingIsRespectedWhenCreatingNewJobs() throws InterruptedException { BulkDataExportOptions options = new BulkDataExportOptions(); options.setExportStyle(BulkDataExportOptions.ExportStyle.SYSTEM); options.setResourceTypes(Sets.newHashSet("Procedure")); @@ -1049,6 +1049,7 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { IBulkDataExportSvc.JobInfo jobInfo6 = myBulkDataExportSvc.submitJob(options, false); IBulkDataExportSvc.JobInfo jobInfo7 = myBulkDataExportSvc.submitJob(options, false); IBulkDataExportSvc.JobInfo jobInfo8 = myBulkDataExportSvc.submitJob(options, false); + Thread.sleep(100L); //stupid commit timings. IBulkDataExportSvc.JobInfo jobInfo9 = myBulkDataExportSvc.submitJob(options, false); //First non-cached should retrieve new ID. From f91a4f9576cc6fcc3350bcf6c79663ac355e557c Mon Sep 17 00:00:00 2001 From: Tadgh Date: Fri, 16 Apr 2021 14:18:01 -0400 Subject: [PATCH 23/39] wip tidy implementaion --- .../jpa/packages/PackageInstallerSvcImpl.java | 2 +- .../partition/RequestPartitionHelperSvc.java | 30 ++++++++----------- .../jpa/bulk/BulkDataExportSvcImplR4Test.java | 6 +++- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java index beb787dc32e..7c044f667db 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/packages/PackageInstallerSvcImpl.java @@ -347,7 +347,7 @@ public class PackageInstallerSvcImpl implements IPackageInstallerSvc { private IBundleProvider searchResource(IFhirResourceDao theDao, SearchParameterMap theMap) { if (myPartitionSettings.isPartitioningEnabled()) { SystemRequestDetails requestDetails = new SystemRequestDetails(); - requestDetails.setTenantId(JpaConstants.DEFAULT_PARTITION_NAME); +// requestDetails.setTenantId(JpaConstants.DEFAULT_PARTITION_NAME); return theDao.search(theMap, requestDetails); } else { return theDao.search(theMap); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java index ceecbb9458d..05707da2e10 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java @@ -103,10 +103,6 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { return RequestPartitionId.defaultPartition(); } - //Shortcircuit and write system calls out to default partition. - if (theRequest instanceof SystemRequestDetails) { - return getSystemRequestPartitionId(theRequest); - } // Interceptor call: STORAGE_PARTITION_IDENTIFY_READ if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_READ, myInterceptorBroadcaster, theRequest)) { @@ -118,6 +114,10 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { requestPartitionId = null; } + if (theRequest instanceof SystemRequestDetails) { + requestPartitionId = getSystemRequestPartitionId(theRequest); + } + validateRequestPartitionNotNull(requestPartitionId, Pointcut.STORAGE_PARTITION_IDENTIFY_READ); return validateNormalizeAndNotifyHooksForRead(requestPartitionId, theRequest); @@ -159,23 +159,19 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { if (myPartitionSettings.isPartitioningEnabled()) { - //Shortcircuit and write system calls out to default partition. - if (theRequest instanceof SystemRequestDetails) { - return getSystemRequestPartitionId(theRequest); - } - - - // Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE - HookParams params = new HookParams() - .add(IBaseResource.class, theResource) - .add(RequestDetails.class, theRequest) - .addIfMatchesType(ServletRequestDetails.class, theRequest); - requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params); // Handle system requests boolean nonPartitionableResource = myNonPartitionableResourceNames.contains(theResourceType); - if (nonPartitionableResource && requestPartitionId == null) { + if (nonPartitionableResource) { requestPartitionId = RequestPartitionId.defaultPartition(); + } else if(theRequest instanceof SystemRequestDetails) { + requestPartitionId = getSystemRequestPartitionId(theRequest); + } else { + HookParams params = new HookParams()// Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE + .add(IBaseResource.class, theResource) + .add(RequestDetails.class, theRequest) + .addIfMatchesType(ServletRequestDetails.class, theRequest); + requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params); } String resourceName = myFhirContext.getResourceType(theResource); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java index 432bab10282..ede4c871584 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java @@ -18,6 +18,7 @@ import ca.uhn.fhir.jpa.entity.BulkExportCollectionEntity; import ca.uhn.fhir.jpa.entity.BulkExportCollectionFileEntity; import ca.uhn.fhir.jpa.entity.BulkExportJobEntity; import ca.uhn.fhir.jpa.entity.MdmLink; +import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.partition.SystemRequestDetails; import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; @@ -1104,6 +1105,7 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { assertThat(nextContents, is(containsString("IMM999"))); assertThat(nextContents, is(not(containsString("Flu")))); + myPartitionSettings.setPartitioningEnabled(false); } private void createResources() { @@ -1113,7 +1115,9 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { //Manually create a golden record Patient goldenPatient = new Patient(); goldenPatient.setId("PAT999"); - DaoMethodOutcome g1Outcome = myPatientDao.update(goldenPatient, new SystemRequestDetails()); + SystemRequestDetails srd = new SystemRequestDetails(); + srd.setTenantId(JpaConstants.ALL_PARTITIONS_NAME); + DaoMethodOutcome g1Outcome = myPatientDao.update(goldenPatient, srd); Long goldenPid = myIdHelperService.getPidOrNull(g1Outcome.getResource()); //Create our golden records' data. From 3075a9b5e6c7cc537db77608ef3717149b81f92b Mon Sep 17 00:00:00 2001 From: Tadgh Date: Fri, 16 Apr 2021 16:53:23 -0400 Subject: [PATCH 24/39] Still minor refactoring --- ...revent-bulk-failure-while-partitioned.yaml | 4 ++ .../partition/RequestPartitionHelperSvc.java | 55 ++++++++++++++----- .../jpa/dao/r4/PartitioningSqlR4Test.java | 1 - 3 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2556-prevent-bulk-failure-while-partitioned.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2556-prevent-bulk-failure-while-partitioned.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2556-prevent-bulk-failure-while-partitioned.yaml new file mode 100644 index 00000000000..facbd901d31 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2556-prevent-bulk-failure-while-partitioned.yaml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 2556 +title: "Fixed a bug which would cause Bulk Export to fail when run in a partitioned environment." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java index 05707da2e10..7e4bf4434eb 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java @@ -36,6 +36,7 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.Nonnull; @@ -49,8 +50,11 @@ import static ca.uhn.fhir.jpa.model.util.JpaConstants.ALL_PARTITIONS_NAME; import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.doCallHooks; import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.doCallHooksAndReturnObject; import static ca.uhn.fhir.jpa.util.JpaInterceptorBroadcaster.hasHooks; +import static org.slf4j.LoggerFactory.getLogger; public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { + private static final Logger ourLog = getLogger(RequestPartitionHelperSvc.class); + private final HashSet myNonPartitionableResourceNames; @@ -97,15 +101,18 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { public RequestPartitionId determineReadPartitionForRequest(@Nullable RequestDetails theRequest, String theResourceType) { RequestPartitionId requestPartitionId; + boolean nonPartitionableResource = myNonPartitionableResourceNames.contains(theResourceType); if (myPartitionSettings.isPartitioningEnabled()) { // Handle system requests - if ((theRequest == null && myNonPartitionableResourceNames.contains(theResourceType))) { + //TODO GGG eventually, theRequest will not be allowed to be null here, and we will pass through SystemRequestDetails instead. + if (theRequest == null && nonPartitionableResource) { return RequestPartitionId.defaultPartition(); } - - // Interceptor call: STORAGE_PARTITION_IDENTIFY_READ - if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_READ, myInterceptorBroadcaster, theRequest)) { + if (theRequest instanceof SystemRequestDetails) { + requestPartitionId = getSystemRequestPartitionId(theRequest, nonPartitionableResource); + // Interceptor call: STORAGE_PARTITION_IDENTIFY_READ + } else if (hasHooks(Pointcut.STORAGE_PARTITION_IDENTIFY_READ, myInterceptorBroadcaster, theRequest)) { HookParams params = new HookParams() .add(RequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest); @@ -114,10 +121,6 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { requestPartitionId = null; } - if (theRequest instanceof SystemRequestDetails) { - requestPartitionId = getSystemRequestPartitionId(theRequest); - } - validateRequestPartitionNotNull(requestPartitionId, Pointcut.STORAGE_PARTITION_IDENTIFY_READ); return validateNormalizeAndNotifyHooksForRead(requestPartitionId, theRequest); @@ -126,6 +129,26 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { return RequestPartitionId.allPartitions(); } + /** + * + * For system requests, read partition from tenant ID if present, otherwise set to DEFAULT. If the resource they are attempting to partition + * is non-partitionable scream in the logs and set the partition to DEFAULT. + * + * @param theRequest + * @param theNonPartitionableResource + * @return + */ + @NotNull + private RequestPartitionId getSystemRequestPartitionId(@NotNull RequestDetails theRequest, boolean theNonPartitionableResource) { + RequestPartitionId requestPartitionId; + requestPartitionId = getSystemRequestPartitionId(theRequest); + if (theNonPartitionableResource && !requestPartitionId.isDefaultPartition()) { + ourLog.warn("System call is attempting to write a non-partitionable resource to a partition! This is a bug in your code! Setting partition to DEFAULT"); + requestPartitionId = RequestPartitionId.defaultPartition(); + } + return requestPartitionId; + } + /** * Determine the partition for a System Call (defined by the fact that the request is of type SystemRequestDetails) * @@ -158,20 +181,22 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { RequestPartitionId requestPartitionId; if (myPartitionSettings.isPartitioningEnabled()) { - - - // Handle system requests boolean nonPartitionableResource = myNonPartitionableResourceNames.contains(theResourceType); - if (nonPartitionableResource) { - requestPartitionId = RequestPartitionId.defaultPartition(); - } else if(theRequest instanceof SystemRequestDetails) { - requestPartitionId = getSystemRequestPartitionId(theRequest); + + if (theRequest instanceof SystemRequestDetails) { + requestPartitionId = getSystemRequestPartitionId(theRequest, nonPartitionableResource); } else { + //This is an external Request (e.g. ServletRequestDetails) so we want to figure out the partition via interceptor. HookParams params = new HookParams()// Interceptor call: STORAGE_PARTITION_IDENTIFY_CREATE .add(IBaseResource.class, theResource) .add(RequestDetails.class, theRequest) .addIfMatchesType(ServletRequestDetails.class, theRequest); requestPartitionId = (RequestPartitionId) doCallHooksAndReturnObject(myInterceptorBroadcaster, theRequest, Pointcut.STORAGE_PARTITION_IDENTIFY_CREATE, params); + + //If the interceptors haven't selected a partition, and its a non-partitionable resource anyhow, send to DEFAULT + if (nonPartitionableResource && requestPartitionId == null) { + requestPartitionId = RequestPartitionId.defaultPartition(); + } } String resourceName = myFhirContext.getResourceType(theResource); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java index cb5691a51be..c8814ae0e4a 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java @@ -642,7 +642,6 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test { assertEquals(myPartitionId, resourceTable.getPartitionId().getPartitionId().intValue()); assertEquals(myPartitionDate, resourceTable.getPartitionId().getPartitionDate()); }); - } @Test From 77e2768a14d3e8f3396480439ee43c6f77f438b6 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Sun, 18 Apr 2021 17:36:55 -0400 Subject: [PATCH 25/39] Address code review comments. Create new static builder for all partitions SRD --- .../jpa/bulk/export/job/GroupBulkItemReader.java | 13 ++++--------- .../jpa/partition/RequestPartitionHelperSvc.java | 3 +-- .../fhir/jpa/partition/SystemRequestDetails.java | 11 +++++++---- .../fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java | 4 +--- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java index ca6e3cbe699..6b49a2134b7 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/bulk/export/job/GroupBulkItemReader.java @@ -55,8 +55,6 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import static ca.uhn.fhir.jpa.model.util.JpaConstants.ALL_PARTITIONS_NAME; - /** * Bulk Item reader for the Group Bulk Export job. * Instead of performing a normal query on the resource type using type filters, we instead @@ -121,8 +119,7 @@ public class GroupBulkItemReader extends BaseBulkItemReader implements ItemReade Set patientPidsToExport = new HashSet<>(pidsOrThrowException); if (myMdmEnabled) { - SystemRequestDetails srd = new SystemRequestDetails(); - srd.setTenantId(ALL_PARTITIONS_NAME); + SystemRequestDetails srd = SystemRequestDetails.newSystemRequestAllPartitions(); IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId), srd); Long pidOrNull = myIdHelperService.getPidOrNull(group); List goldenPidSourcePidTuple = myMdmLinkDao.expandPidsFromGroupPidGivenMatchResult(pidOrNull, MdmMatchResultEnum.MATCH); @@ -183,8 +180,7 @@ public class GroupBulkItemReader extends BaseBulkItemReader implements ItemReade * @return A list of strings representing the Patient IDs of the members (e.g. ["P1", "P2", "P3"] */ private List getMembers() { - SystemRequestDetails requestDetails = new SystemRequestDetails(); - requestDetails.setTenantId(ALL_PARTITIONS_NAME); + SystemRequestDetails requestDetails = SystemRequestDetails.newSystemRequestAllPartitions(); IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId), requestDetails); List evaluate = myContext.newFhirPath().evaluate(group, "member.entity.reference", IPrimitiveType.class); return evaluate.stream().map(IPrimitiveType::getValueAsString).collect(Collectors.toList()); @@ -199,9 +195,8 @@ public class GroupBulkItemReader extends BaseBulkItemReader implements ItemReade */ private Set expandAllPatientPidsFromGroup() { Set expandedIds = new HashSet<>(); - SystemRequestDetails requestDetails = new SystemRequestDetails(); - requestDetails.setTenantId(ALL_PARTITIONS_NAME); - IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId), new SystemRequestDetails()); + SystemRequestDetails requestDetails = SystemRequestDetails.newSystemRequestAllPartitions(); + IBaseResource group = myDaoRegistry.getResourceDao("Group").read(new IdDt(myGroupId), requestDetails); Long pidOrNull = myIdHelperService.getPidOrNull(group); //Attempt to perform MDM Expansion of membership diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java index 7e4bf4434eb..4bcf6b1ddb5 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java @@ -143,8 +143,7 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { RequestPartitionId requestPartitionId; requestPartitionId = getSystemRequestPartitionId(theRequest); if (theNonPartitionableResource && !requestPartitionId.isDefaultPartition()) { - ourLog.warn("System call is attempting to write a non-partitionable resource to a partition! This is a bug in your code! Setting partition to DEFAULT"); - requestPartitionId = RequestPartitionId.defaultPartition(); + throw new InternalErrorException("System call is attempting to write a non-partitionable resource to a partition! This is a bug!") } return requestPartitionId; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/SystemRequestDetails.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/SystemRequestDetails.java index c2b3361eb0c..f194a1d8f73 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/SystemRequestDetails.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/SystemRequestDetails.java @@ -35,17 +35,15 @@ import ca.uhn.fhir.rest.server.IRestfulServerDefaults; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableListMultimap; -import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ListMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.Multimaps; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.nio.charset.Charset; import java.util.List; -import java.util.Optional; + +import static ca.uhn.fhir.jpa.model.util.JpaConstants.ALL_PARTITIONS_NAME; /** * A default RequestDetails implementation that can be used for system calls to @@ -104,6 +102,11 @@ public class SystemRequestDetails extends RequestDetails { } myHeaders.put(theName, theValue); } + public static SystemRequestDetails newSystemRequestAllPartitions() { + SystemRequestDetails systemRequestDetails = new SystemRequestDetails(); + systemRequestDetails.setTenantId(ALL_PARTITIONS_NAME); + return systemRequestDetails; + } @Override diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java index ede4c871584..701de70bb04 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/bulk/BulkDataExportSvcImplR4Test.java @@ -18,7 +18,6 @@ import ca.uhn.fhir.jpa.entity.BulkExportCollectionEntity; import ca.uhn.fhir.jpa.entity.BulkExportCollectionFileEntity; import ca.uhn.fhir.jpa.entity.BulkExportJobEntity; import ca.uhn.fhir.jpa.entity.MdmLink; -import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.partition.SystemRequestDetails; import ca.uhn.fhir.mdm.api.MdmLinkSourceEnum; import ca.uhn.fhir.mdm.api.MdmMatchResultEnum; @@ -1115,8 +1114,7 @@ public class BulkDataExportSvcImplR4Test extends BaseBatchJobR4Test { //Manually create a golden record Patient goldenPatient = new Patient(); goldenPatient.setId("PAT999"); - SystemRequestDetails srd = new SystemRequestDetails(); - srd.setTenantId(JpaConstants.ALL_PARTITIONS_NAME); + SystemRequestDetails srd = SystemRequestDetails.newSystemRequestAllPartitions(); DaoMethodOutcome g1Outcome = myPatientDao.update(goldenPatient, srd); Long goldenPid = myIdHelperService.getPidOrNull(g1Outcome.getResource()); From c54862e77293334f3b8f970eeda8487265a0bb31 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Sun, 18 Apr 2021 18:03:37 -0400 Subject: [PATCH 26/39] Coding around at the speed of sound occasionally causes compilation failures --- .../ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java index 4bcf6b1ddb5..ce725f71515 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java @@ -143,7 +143,7 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { RequestPartitionId requestPartitionId; requestPartitionId = getSystemRequestPartitionId(theRequest); if (theNonPartitionableResource && !requestPartitionId.isDefaultPartition()) { - throw new InternalErrorException("System call is attempting to write a non-partitionable resource to a partition! This is a bug!") + throw new InternalErrorException("System call is attempting to write a non-partitionable resource to a partition! This is a bug!"); } return requestPartitionId; } From 89e56ecb98fa84e3969471ea178085f890564c1f Mon Sep 17 00:00:00 2001 From: Tadgh Date: Mon, 19 Apr 2021 09:14:12 -0400 Subject: [PATCH 27/39] Remove jetbrains annotation --- .../ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java index ce725f71515..0daeaf201cd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java @@ -35,7 +35,6 @@ import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; @@ -138,8 +137,7 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { * @param theNonPartitionableResource * @return */ - @NotNull - private RequestPartitionId getSystemRequestPartitionId(@NotNull RequestDetails theRequest, boolean theNonPartitionableResource) { + private RequestPartitionId getSystemRequestPartitionId(RequestDetails theRequest, boolean theNonPartitionableResource) { RequestPartitionId requestPartitionId; requestPartitionId = getSystemRequestPartitionId(theRequest); if (theNonPartitionableResource && !requestPartitionId.isDefaultPartition()) { From 52d161c3376e28e65b29a5aff7e548662eec82e8 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Mon, 19 Apr 2021 09:16:35 -0400 Subject: [PATCH 28/39] Add checkstyle to prevent future jetbrains annotations from sneaking in --- src/checkstyle/checkstyle.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/checkstyle/checkstyle.xml b/src/checkstyle/checkstyle.xml index 1c8800c913c..15d7b80d7a0 100644 --- a/src/checkstyle/checkstyle.xml +++ b/src/checkstyle/checkstyle.xml @@ -30,6 +30,16 @@ + + + + + + + + + + From 014fa052714a118f4004ca1e1b123e24ff4896f0 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Mon, 19 Apr 2021 09:17:35 -0400 Subject: [PATCH 29/39] Fix a present annotation --- .../java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java index 89a2dfbbfbf..c21285ab398 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/SearchParamExtractorR4Test.java @@ -37,12 +37,12 @@ import org.hl7.fhir.r4.model.Quantity; import org.hl7.fhir.r4.model.Reference; import org.hl7.fhir.r4.model.SearchParameter; import org.hl7.fhir.r4.model.StringType; -import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; From 2ed4b01eb8f0dee52d7b53cb41aae5505ac3f3d6 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Mon, 19 Apr 2021 10:31:16 -0400 Subject: [PATCH 30/39] Remove notnull --- .../ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java index 0daeaf201cd..326111da44f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/partition/RequestPartitionHelperSvc.java @@ -156,8 +156,8 @@ public class RequestPartitionHelperSvc implements IRequestPartitionHelperSvc { * @param theRequest The {@link SystemRequestDetails} * @return the {@link RequestPartitionId} to be used for this request. */ - @NotNull - private RequestPartitionId getSystemRequestPartitionId(@NotNull RequestDetails theRequest) { + @Nonnull + private RequestPartitionId getSystemRequestPartitionId(@Nonnull RequestDetails theRequest) { if (theRequest.getTenantId() != null) { if (theRequest.getTenantId().equals(ALL_PARTITIONS_NAME)) { return RequestPartitionId.allPartitions(); From 536ca0e73dac76a9d0cee397ee1d0f70d539df3e Mon Sep 17 00:00:00 2001 From: Nick Goupinets Date: Mon, 19 Apr 2021 16:24:52 -0400 Subject: [PATCH 31/39] Code review updates --- .../java/ca/uhn/fhir/util/TerserUtil.java | 63 +++++++++++++------ ...15-mdm-survivorship-rules-application.yaml | 2 +- .../java/ca/uhn/fhir/util/TerserUtilTest.java | 8 ++- 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtil.java index 126b2dfca3b..e6de9a87c0d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/TerserUtil.java @@ -49,9 +49,15 @@ public final class TerserUtil { private static final String EQUALS_DEEP = "equalsDeep"; + /** + * Exclude for id, identifier and meta fields of a resource. + */ public static final Collection IDS_AND_META_EXCLUDES = Collections.unmodifiableSet(Stream.of("id", "identifier", "meta").collect(Collectors.toSet())); + /** + * Exclusion predicate for id, identifier, meta fields. + */ public static final Predicate EXCLUDE_IDS_AND_META = new Predicate() { @Override public boolean test(String s) { @@ -59,17 +65,25 @@ public final class TerserUtil { } }; + /** + * Exclusion predicate for id/identifier, meta and fields with empty values. This ensures that source / target resources, + * empty source fields will not results in erasure of target fields. + */ public static final Predicate> EXCLUDE_IDS_META_AND_EMPTY = new Predicate>() { @Override public boolean test(Triple theTriple) { if (!EXCLUDE_IDS_AND_META.test(theTriple.getLeft().getElementName())) { return false; } - - return theTriple.getLeft().getAccessor().getValues(theTriple.getRight()).isEmpty(); + BaseRuntimeChildDefinition childDefinition = theTriple.getLeft(); + boolean isSourceFieldEmpty = childDefinition.getAccessor().getValues(theTriple.getMiddle()).isEmpty(); + return !isSourceFieldEmpty; } }; + /** + * Exclusion predicate for keeping all fields. + */ public static final Predicate INCLUDE_ALL = new Predicate() { @Override public boolean test(String s) { @@ -262,8 +276,8 @@ public final class TerserUtil { } /** - * Replaces empty fields on theTo resource that test positive by the given predicate. theTo will contain a copy of the - * values from theFrom for which predicate tests positive. + * Replaces fields on theTo resource that test positive by the given predicate. theTo will contain a copy of the + * values from theFrom for which predicate tests positive. Please note that composite fields will be replaced fully. * * @param theFhirContext Context holding resource definition * @param theFrom The resource to merge the fields from @@ -271,15 +285,11 @@ public final class TerserUtil { * @param thePredicate Predicate that checks if a given field should be replaced */ public static void replaceFieldsByPredicate(FhirContext theFhirContext, IBaseResource theFrom, IBaseResource theTo, Predicate> thePredicate) { - FhirTerser terser = theFhirContext.newTerser(); - RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom); for (BaseRuntimeChildDefinition childDefinition : definition.getChildrenAndExtension()) { - if (!thePredicate.test(Triple.of(childDefinition, theFrom, theTo))) { - continue; + if (thePredicate.test(Triple.of(childDefinition, theFrom, theTo))) { + replaceField(theFrom, theTo, childDefinition); } - - replaceField(theFrom, theTo, childDefinition); } } @@ -304,14 +314,11 @@ public final class TerserUtil { * @param theTo The resource to replace the field on */ public static void replaceField(FhirContext theFhirContext, String theFieldName, IBaseResource theFrom, IBaseResource theTo) { - replaceField(theFhirContext, theFhirContext.newTerser(), theFieldName, theFrom, theTo); - } - - /** - * @deprecated Use {@link #replaceField(FhirContext, String, IBaseResource, IBaseResource)} instead - */ - public static void replaceField(FhirContext theFhirContext, FhirTerser theTerser, String theFieldName, IBaseResource theFrom, IBaseResource theTo) { - replaceField(theFrom, theTo, getBaseRuntimeChildDefinition(theFhirContext, theFieldName, theFrom)); + RuntimeResourceDefinition definition = theFhirContext.getResourceDefinition(theFrom); + if (definition == null) { + throw new IllegalArgumentException(String.format("Field %s does not exist in %s", theFieldName, theFrom)); + } + replaceField(theFrom, theTo, theFhirContext.getResourceDefinition(theFrom).getChildByName(theFieldName)); } /** @@ -328,7 +335,7 @@ public final class TerserUtil { /** * Sets the provided field with the given values. This method will add to the collection of existing field values - * in case of multiple cardinality. Use {@link #clearField(FhirContext, FhirTerser, String, IBaseResource, IBase...)} + * in case of multiple cardinality. Use {@link #clearField(FhirContext, String, IBaseResource)} * to remove values before setting * * @param theFhirContext Context holding resource definition @@ -342,7 +349,7 @@ public final class TerserUtil { /** * Sets the provided field with the given values. This method will add to the collection of existing field values - * in case of multiple cardinality. Use {@link #clearField(FhirContext, FhirTerser, String, IBaseResource, IBase...)} + * in case of multiple cardinality. Use {@link #clearField(FhirContext, String, IBaseResource)} * to remove values before setting * * @param theFhirContext Context holding resource definition @@ -397,10 +404,26 @@ public final class TerserUtil { setFieldByFhirPath(theFhirContext.newTerser(), theFhirPath, theResource, theValue); } + /** + * Returns field values ant the specified FHIR path from the resource. + * + * @param theFhirContext Context holding resource definition + * @param theFhirPath The FHIR path to get the field from + * @param theResource The resource from which the value should be retrieved + * @return Returns the list of field values at the given FHIR path + */ public static List getFieldByFhirPath(FhirContext theFhirContext, String theFhirPath, IBase theResource) { return theFhirContext.newTerser().getValues(theResource, theFhirPath, false, false); } + /** + * Returns the first available field value at the specified FHIR path from the resource. + * + * @param theFhirContext Context holding resource definition + * @param theFhirPath The FHIR path to get the field from + * @param theResource The resource from which the value should be retrieved + * @return Returns the first available value or null if no values can be retrieved + */ public static IBase getFirstFieldByFhirPath(FhirContext theFhirContext, String theFhirPath, IBase theResource) { List values = getFieldByFhirPath(theFhirContext, theFhirPath, theResource); if (values == null || values.isEmpty()) { diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2515-mdm-survivorship-rules-application.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2515-mdm-survivorship-rules-application.yaml index aa93351b4b4..f8e96a9f02e 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2515-mdm-survivorship-rules-application.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2515-mdm-survivorship-rules-application.yaml @@ -1,4 +1,4 @@ --- type: fix issue: 2515 -title: "Addresses MDM survivorship rules application on matching a single resource" +title: "Fixed issues with application of survivorship rules when matching golden record to a single resource" diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/TerserUtilTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/TerserUtilTest.java index a94efbfb845..0d9d03e6f32 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/TerserUtilTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/TerserUtilTest.java @@ -13,6 +13,8 @@ import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.PrimitiveType; import org.junit.jupiter.api.Test; +import java.util.Date; + import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.junit.jupiter.api.Assertions.*; @@ -303,12 +305,16 @@ class TerserUtilTest { Patient p2 = new Patient(); p2.addName().setFamily("Smith"); + Date dob = new Date(); + p2.setBirthDate(dob); TerserUtil.replaceFieldsByPredicate(ourFhirContext, p1, p2, TerserUtil.EXCLUDE_IDS_META_AND_EMPTY); + // expect p2 to have "Doe" and MALE after replace assertEquals(1, p2.getName().size()); - assertEquals("Smith", p2.getName().get(0).getFamily()); + assertEquals("Doe", p2.getName().get(0).getFamily()); assertEquals(Enumerations.AdministrativeGender.MALE, p2.getGender()); + assertEquals(dob, p2.getBirthDate()); } @Test From cadca518ec3a8a6d55992a82eff6ddd5e135858c Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Tue, 20 Apr 2021 09:14:50 -0400 Subject: [PATCH 32/39] Add options to BaseCommand --- .../main/java/ca/uhn/fhir/cli/BaseCommand.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java index b2b9cb837c3..2e511c26433 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java @@ -31,6 +31,7 @@ import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.time.DateUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -70,6 +71,9 @@ public abstract class BaseCommand implements Comparable { protected static final String VERBOSE_LOGGING_PARAM = "l"; protected static final String VERBOSE_LOGGING_PARAM_LONGOPT = "logging"; protected static final String VERBOSE_LOGGING_PARAM_DESC = "If specified, verbose logging will be used."; + protected static final int DEFAULT_THREAD_COUNT = 10; + protected static final String THREAD_COUNT = "thread-count"; + // TODO: Don't use qualified names for loggers in HAPI CLI. private static final Logger ourLog = LoggerFactory.getLogger(BaseCommand.class); protected FhirContext myFhirCtx; @@ -87,6 +91,11 @@ public abstract class BaseCommand implements Comparable { addOptionalOption(theOptions, null, BEARER_TOKEN_PARAM_LONGOPT, BEARER_TOKEN_PARAM_NAME, BEARER_TOKEN_PARAM_DESC); } + protected void addThreadCountOption(Options theOptions) { + addOptionalOption(theOptions, null, THREAD_COUNT, "count", "If specified, this argument specifies the number of worker threads used (default is " + DEFAULT_THREAD_COUNT + ")"); + } + + protected String promptUser(String thePrompt) throws ParseException { System.out.print(ansi().bold().fgBrightDefault()); System.out.print(thePrompt); @@ -309,6 +318,12 @@ public abstract class BaseCommand implements Comparable { return getFhirContext().getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class); } + protected int getThreadCount(CommandLine theCommandLine) throws ParseException { + Integer parallelismThreadCount = getAndParsePositiveIntegerParam(theCommandLine, THREAD_COUNT); + parallelismThreadCount = ObjectUtils.defaultIfNull(parallelismThreadCount, DEFAULT_THREAD_COUNT); + return parallelismThreadCount.intValue(); + } + public abstract String getCommandDescription(); public abstract String getCommandName(); From 892ed5a3465cc9e504eba1f14b3ade4afd0faea5 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Tue, 20 Apr 2021 11:15:37 -0400 Subject: [PATCH 33/39] Add options to BaseCommand (#2563) --- .../main/java/ca/uhn/fhir/cli/BaseCommand.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java index b2b9cb837c3..2e511c26433 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/BaseCommand.java @@ -31,6 +31,7 @@ import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.time.DateUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -70,6 +71,9 @@ public abstract class BaseCommand implements Comparable { protected static final String VERBOSE_LOGGING_PARAM = "l"; protected static final String VERBOSE_LOGGING_PARAM_LONGOPT = "logging"; protected static final String VERBOSE_LOGGING_PARAM_DESC = "If specified, verbose logging will be used."; + protected static final int DEFAULT_THREAD_COUNT = 10; + protected static final String THREAD_COUNT = "thread-count"; + // TODO: Don't use qualified names for loggers in HAPI CLI. private static final Logger ourLog = LoggerFactory.getLogger(BaseCommand.class); protected FhirContext myFhirCtx; @@ -87,6 +91,11 @@ public abstract class BaseCommand implements Comparable { addOptionalOption(theOptions, null, BEARER_TOKEN_PARAM_LONGOPT, BEARER_TOKEN_PARAM_NAME, BEARER_TOKEN_PARAM_DESC); } + protected void addThreadCountOption(Options theOptions) { + addOptionalOption(theOptions, null, THREAD_COUNT, "count", "If specified, this argument specifies the number of worker threads used (default is " + DEFAULT_THREAD_COUNT + ")"); + } + + protected String promptUser(String thePrompt) throws ParseException { System.out.print(ansi().bold().fgBrightDefault()); System.out.print(thePrompt); @@ -309,6 +318,12 @@ public abstract class BaseCommand implements Comparable { return getFhirContext().getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class); } + protected int getThreadCount(CommandLine theCommandLine) throws ParseException { + Integer parallelismThreadCount = getAndParsePositiveIntegerParam(theCommandLine, THREAD_COUNT); + parallelismThreadCount = ObjectUtils.defaultIfNull(parallelismThreadCount, DEFAULT_THREAD_COUNT); + return parallelismThreadCount.intValue(); + } + public abstract String getCommandDescription(); public abstract String getCommandName(); From 4e266462ec2e0363bbee8298e34263940eaee286 Mon Sep 17 00:00:00 2001 From: Nick Goupinets Date: Tue, 20 Apr 2021 16:17:27 -0400 Subject: [PATCH 34/39] Bumped up version due to breaking changes --- hapi-deployable-pom/pom.xml | 2 +- hapi-fhir-android/pom.xml | 2 +- hapi-fhir-base/pom.xml | 2 +- hapi-fhir-bom/pom.xml | 4 ++-- hapi-fhir-cli/hapi-fhir-cli-api/pom.xml | 2 +- hapi-fhir-cli/hapi-fhir-cli-app/pom.xml | 2 +- hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml | 2 +- hapi-fhir-cli/pom.xml | 2 +- hapi-fhir-client-okhttp/pom.xml | 2 +- hapi-fhir-client/pom.xml | 2 +- hapi-fhir-converter/pom.xml | 2 +- hapi-fhir-dist/pom.xml | 2 +- hapi-fhir-docs/pom.xml | 8 ++++---- hapi-fhir-jacoco/pom.xml | 2 +- hapi-fhir-jaxrsserver-base/pom.xml | 2 +- hapi-fhir-jaxrsserver-example/pom.xml | 2 +- hapi-fhir-jpaserver-api/pom.xml | 2 +- hapi-fhir-jpaserver-base/pom.xml | 2 +- hapi-fhir-jpaserver-batch/pom.xml | 2 +- hapi-fhir-jpaserver-cql/pom.xml | 6 +++--- hapi-fhir-jpaserver-mdm/pom.xml | 6 +++--- hapi-fhir-jpaserver-migrate/pom.xml | 2 +- hapi-fhir-jpaserver-model/pom.xml | 2 +- hapi-fhir-jpaserver-searchparam/pom.xml | 2 +- hapi-fhir-jpaserver-subscription/pom.xml | 2 +- hapi-fhir-jpaserver-test-utilities/pom.xml | 2 +- hapi-fhir-jpaserver-uhnfhirtest/pom.xml | 4 ++-- hapi-fhir-server-mdm/pom.xml | 2 +- hapi-fhir-server/pom.xml | 2 +- .../hapi-fhir-spring-boot-autoconfigure/pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../hapi-fhir-spring-boot-samples/pom.xml | 2 +- .../hapi-fhir-spring-boot-starter/pom.xml | 2 +- hapi-fhir-spring-boot/pom.xml | 2 +- hapi-fhir-structures-dstu2.1/pom.xml | 2 +- hapi-fhir-structures-dstu2/pom.xml | 2 +- hapi-fhir-structures-dstu3/pom.xml | 2 +- hapi-fhir-structures-hl7org-dstu2/pom.xml | 2 +- hapi-fhir-structures-r4/pom.xml | 2 +- hapi-fhir-structures-r5/pom.xml | 2 +- hapi-fhir-test-utilities/pom.xml | 2 +- hapi-fhir-testpage-overlay/pom.xml | 2 +- hapi-fhir-validation-resources-dstu2.1/pom.xml | 2 +- hapi-fhir-validation-resources-dstu2/pom.xml | 2 +- hapi-fhir-validation-resources-dstu3/pom.xml | 2 +- hapi-fhir-validation-resources-r4/pom.xml | 2 +- hapi-fhir-validation-resources-r5/pom.xml | 2 +- hapi-fhir-validation/pom.xml | 2 +- hapi-tinder-plugin/pom.xml | 16 ++++++++-------- hapi-tinder-test/pom.xml | 2 +- pom.xml | 2 +- restful-server-example/pom.xml | 2 +- .../pom.xml | 2 +- tests/hapi-fhir-base-test-mindeps-client/pom.xml | 2 +- tests/hapi-fhir-base-test-mindeps-server/pom.xml | 2 +- 57 files changed, 73 insertions(+), 73 deletions(-) diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index 2925cddee83..9df4f52f5df 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index 60f23bd7bcc..a03a6465c2a 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 00ce73745e2..912fe4efd95 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index d3c726265b4..fdb411e91dc 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -3,14 +3,14 @@ 4.0.0 ca.uhn.hapi.fhir hapi-fhir-bom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT pom HAPI FHIR BOM ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/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 2816ae6159e..92bafa40399 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-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 7d9dc89fe6c..0c0e9694712 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml index 1055a8ed4ea..23fdc156c0e 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../../hapi-deployable-pom diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index 8784078d56b..ae67b1dad2f 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index cef760eeff8..52fed03fb71 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index e7ac14275c6..f407f56be2d 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index 2ec2f22795c..a988e2f45c5 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index 24b559ea001..8ea8d57f03b 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index 40519292d48..2fcd0c94674 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -78,13 +78,13 @@ ca.uhn.hapi.fhir hapi-fhir-structures-dstu2 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT compile ca.uhn.hapi.fhir hapi-fhir-jpaserver-subscription - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT compile @@ -101,7 +101,7 @@ ca.uhn.hapi.fhir hapi-fhir-testpage-overlay - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT classes diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index 43dd8b85ed7..1d22f0b5e4a 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index 073ecc3dbf0..b2004c3ad14 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-example/pom.xml b/hapi-fhir-jaxrsserver-example/pom.xml index cce18f1d9c1..c3417a5963f 100644 --- a/hapi-fhir-jaxrsserver-example/pom.xml +++ b/hapi-fhir-jaxrsserver-example/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-jpaserver-api/pom.xml b/hapi-fhir-jpaserver-api/pom.xml index ac534aaccba..fa591624b35 100644 --- a/hapi-fhir-jpaserver-api/pom.xml +++ b/hapi-fhir-jpaserver-api/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index eedfef43910..3fed26d92f7 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-batch/pom.xml b/hapi-fhir-jpaserver-batch/pom.xml index a08e196fe26..6b49017689b 100644 --- a/hapi-fhir-jpaserver-batch/pom.xml +++ b/hapi-fhir-jpaserver-batch/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-cql/pom.xml b/hapi-fhir-jpaserver-cql/pom.xml index 20069c8fa7e..9be25f3da9e 100644 --- a/hapi-fhir-jpaserver-cql/pom.xml +++ b/hapi-fhir-jpaserver-cql/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -144,13 +144,13 @@ ca.uhn.hapi.fhir hapi-fhir-test-utilities - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT test ca.uhn.hapi.fhir hapi-fhir-jpaserver-test-utilities - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT test diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-mdm/pom.xml index 3295a49c94a..29d7f7a9503 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -55,13 +55,13 @@ ca.uhn.hapi.fhir hapi-fhir-test-utilities - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT test ca.uhn.hapi.fhir hapi-fhir-jpaserver-test-utilities - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT test diff --git a/hapi-fhir-jpaserver-migrate/pom.xml b/hapi-fhir-jpaserver-migrate/pom.xml index 68316dcf0db..be260a03b39 100644 --- a/hapi-fhir-jpaserver-migrate/pom.xml +++ b/hapi-fhir-jpaserver-migrate/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index b46518bed86..7a40544f634 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index 5dc3b7d17f4..5bc11f73872 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index fc862aea074..369918d69c8 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-utilities/pom.xml b/hapi-fhir-jpaserver-test-utilities/pom.xml index b66f8e9615e..06b73aa7bac 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 183e9a6b602..9cec5a13cf8 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../pom.xml @@ -164,7 +164,7 @@ ca.uhn.hapi.fhir hapi-fhir-converter - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT diff --git a/hapi-fhir-server-mdm/pom.xml b/hapi-fhir-server-mdm/pom.xml index adc4be9c0ab..1952a50d819 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index 1e71fc77421..4fe87a2ee16 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-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 d0905db3a66..19eedc6bfd2 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-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 919e895664c..656d16080d7 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-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 622de27723a..0f9be244025 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT hapi-fhir-spring-boot-sample-client-okhttp 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 ad0f6ed0cfb..9d5b6483f92 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT hapi-fhir-spring-boot-sample-server-jersey 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 02e9d448d79..a57c5e80e1b 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT hapi-fhir-spring-boot-samples 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 c210826be2c..3f90ae6bbcc 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index 38c1789454d..ff3fe4a179a 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index c8054db0e2b..61d4ea265db 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 6659bcd46c2..9359dc2e980 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index 04fc5a8cbc3..5a825a62517 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-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 8383bdda70a..e842ad1c049 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index e0793555c76..d5f9e85f836 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r5/pom.xml b/hapi-fhir-structures-r5/pom.xml index f564ed044b8..490ba126870 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index ac4521d10e5..66f0f0ac9fd 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 0a140bc05ab..072a4f9a21c 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml index 4db0f72591a..f8cc76070d3 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-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 a3b7462547d..a69e2ea236d 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-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 33d67b4e94a..0061ae4ef6e 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-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 5f830ffce69..ace6fe58362 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-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 dbc5e9359b8..442455129ad 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index aafc58b15ba..a441397e88e 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index 6daf487888f..1da19fdaf24 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../pom.xml @@ -58,37 +58,37 @@ ca.uhn.hapi.fhir hapi-fhir-structures-dstu3 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-hl7org-dstu2 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-r4 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-r5 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu2 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu3 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-r4 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT org.apache.velocity diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index 6b9aa6546e9..2f98895c12d 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index a30317cecab..109923b7ecc 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir pom - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT HAPI-FHIR An open-source implementation of the FHIR specification in Java. https://hapifhir.io diff --git a/restful-server-example/pom.xml b/restful-server-example/pom.xml index 7640191ae97..ee4e9459316 100644 --- a/restful-server-example/pom.xml +++ b/restful-server-example/pom.xml @@ -8,7 +8,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../pom.xml diff --git a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml index 2ef7b674d4b..757146f0f1c 100644 --- a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml +++ b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-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 85242f389d3..e295bb2a92c 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-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 9716634f826..bde4aa9378f 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 - 5.4.0-PRE6-SNAPSHOT + 5.4.0-PRE7-SNAPSHOT ../../pom.xml From e711dfb07c60be363613cdb5403ef7ff72cae351 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Wed, 21 Apr 2021 16:44:22 -0400 Subject: [PATCH 35/39] enhance bundlebuilder to support deletes --- .../java/ca/uhn/fhir/util/BundleBuilder.java | 50 +++++++++++++++++++ .../uhn/fhir/validation/PlaceholderTest.java | 1 - .../ca/uhn/fhir/util/BundleBuilderTest.java | 30 +++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java index 7e3c568663e..657c3f23cc4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleBuilder.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.context.BaseRuntimeChildDefinition; import ca.uhn.fhir.context.BaseRuntimeElementDefinition; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.model.primitive.IdDt; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBackboneElement; @@ -194,6 +195,45 @@ public class BundleBuilder { return new CreateBuilder(request); } + /** + * Adds an entry containing a delete (DELETE) request. + * Also sets the Bundle.type value to "transaction" if it is not already set. + * + * Note that the resource is only used to extract its ID and type, and the body of the resource is not included in the entry, + * + * @param theResource The resource to delete. + */ + public void addTransactionDeleteEntry(IBaseResource theResource) { + String resourceType = myContext.getResourceType(theResource); + String idPart = theResource.getIdElement().toUnqualifiedVersionless().getIdPart(); + addTransactionDeleteEntry(resourceType, idPart); + } + + /** + * Adds an entry containing a delete (DELETE) request. + * Also sets the Bundle.type value to "transaction" if it is not already set. + * + * @param theResourceType The type resource to delete. + * @param theIdPart the ID of the resource to delete. + */ + public void addTransactionDeleteEntry(String theResourceType, String theIdPart) { + setBundleField("type", "transaction"); + IBase request = addEntryAndReturnRequest(); + IdDt idDt = new IdDt(theIdPart); + + // Bundle.entry.request.url + IPrimitiveType url = (IPrimitiveType) myContext.getElementDefinition("uri").newInstance(); + url.setValueAsString(idDt.toUnqualifiedVersionless().withResourceType(theResourceType).getValue()); + myEntryRequestUrlChild.getMutator().setValue(request, url); + + // Bundle.entry.request.method + IPrimitiveType method = (IPrimitiveType) myEntryRequestMethodDef.newInstance(myEntryRequestMethodChild.getInstanceConstructorArguments()); + method.setValueAsString("DELETE"); + myEntryRequestMethodChild.getMutator().setValue(request, method); + } + + + /** * Adds an entry for a Collection bundle type */ @@ -251,6 +291,16 @@ public class BundleBuilder { return request; } + public IBase addEntryAndReturnRequest() { + IBase entry = addEntry(); + + // Bundle.entry.request + IBase request = myEntryRequestDef.newInstance(); + myEntryRequestChild.getMutator().setValue(entry, request); + return request; + + } + public IBaseBundle getBundle() { return myBundle; diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/validation/PlaceholderTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/validation/PlaceholderTest.java index f757cc2b801..5d5b2820135 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/validation/PlaceholderTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/validation/PlaceholderTest.java @@ -11,5 +11,4 @@ public class PlaceholderTest { public void testPass() { // nothing } - } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/BundleBuilderTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/BundleBuilderTest.java index 22bf634d525..e31bbc9fbd0 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/BundleBuilderTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/util/BundleBuilderTest.java @@ -156,6 +156,36 @@ public class BundleBuilderTest { assertEquals(Bundle.HTTPVerb.POST, bundle.getEntry().get(0).getRequest().getMethod()); } + @Test + public void testAddEntryDelete() { + BundleBuilder builder = new BundleBuilder(myFhirContext); + + Patient patient = new Patient(); + patient.setActive(true); + patient.setId("123"); + builder.addTransactionDeleteEntry(patient); + builder.addTransactionDeleteEntry("Patient", "123"); + Bundle bundle = (Bundle) builder.getBundle(); + + ourLog.info("Bundle:\n{}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle)); + + assertEquals(Bundle.BundleType.TRANSACTION, bundle.getType()); + assertEquals(2, bundle.getEntry().size()); + + //Check the IBaseresource style entry + assertNull(bundle.getEntry().get(0).getResource()); + assertEquals("Patient/123", bundle.getEntry().get(0).getRequest().getUrl()); + assertEquals(Bundle.HTTPVerb.DELETE, bundle.getEntry().get(0).getRequest().getMethod()); + + //Check the resourcetype + id style entry. + assertNull(bundle.getEntry().get(1).getResource()); + assertEquals("Patient/123", bundle.getEntry().get(1).getRequest().getUrl()); + assertEquals(Bundle.HTTPVerb.DELETE, bundle.getEntry().get(1).getRequest().getMethod()); + + + + } + @Test public void testAddEntryCreateConditional() { BundleBuilder builder = new BundleBuilder(myFhirContext); From 9c19a4c087a1289477819cd4cc1da80c158f2428 Mon Sep 17 00:00:00 2001 From: Tadgh Date: Fri, 23 Apr 2021 09:11:36 -0400 Subject: [PATCH 36/39] Add changelog --- .../changelog/5_4_0/2571-add-delete-to-bundle-builder.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2571-add-delete-to-bundle-builder.yaml diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2571-add-delete-to-bundle-builder.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2571-add-delete-to-bundle-builder.yaml new file mode 100644 index 00000000000..6da48fc802c --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2571-add-delete-to-bundle-builder.yaml @@ -0,0 +1,4 @@ +--- +type: fix +issue: 2571 +title: "Added support for deleting resources to BundleBuilder via method `addTransactionDeleteEntry`." From 2ba100576263f07e8733b733d90ca8046ae33da4 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Sun, 25 Apr 2021 15:40:50 -0400 Subject: [PATCH 37/39] OpenAPI Support (#2560) * Start work on OpenAPI * Fixes * Work on OpenAPI * Cleanup * Cleanup * More swagger work * Build fix * More work * More work * Add documentation * Docs fixes * Add changelog * License updates * Add API * Cleanup * Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/openapi.md Co-authored-by: patrick-vachon-smilecdr <81274188+patrick-vachon-smilecdr@users.noreply.github.com> * Work on scripts * Add docs * Compile fix * Work on fixes * Fix tests * Test fix * Test fix * Build fix * Tests * Build fix * Work on pipeline * Version bump * Test fix * Version bump * Test fix attempts * Test fix * Test fix * Remove accidentally committed files * Fixes * Tets fixes * Test fix * Test fix * Test fix * Test fixes * Test fixes * Test fixes * License header updates * test fix * Test fixes * Test fix * Test fixes * Test fix * Checkstyle bump Co-authored-by: patrick-vachon-smilecdr <81274188+patrick-vachon-smilecdr@users.noreply.github.com> --- hapi-deployable-pom/pom.xml | 2 +- hapi-fhir-android/pom.xml | 2 +- hapi-fhir-base/pom.xml | 2 +- .../BaseRuntimeDeclaredChildDefinition.java | 3 +- .../model/api/annotation/Description.java | 24 +- .../ca/uhn/fhir/rest/annotation/AddTags.java | 9 + .../ca/uhn/fhir/rest/annotation/Create.java | 11 +- .../ca/uhn/fhir/rest/annotation/Delete.java | 13 +- .../uhn/fhir/rest/annotation/DeleteTags.java | 10 + .../ca/uhn/fhir/rest/annotation/History.java | 12 +- .../uhn/fhir/rest/annotation/Operation.java | 5 +- .../fhir/rest/annotation/OperationParam.java | 4 +- .../ca/uhn/fhir/rest/annotation/Patch.java | 9 + .../ca/uhn/fhir/rest/annotation/Search.java | 11 + .../ca/uhn/fhir/rest/annotation/Update.java | 14 +- .../ca/uhn/fhir/rest/annotation/Validate.java | 12 +- .../java/ca/uhn/fhir/util/ExtensionUtil.java | 47 +- .../java/ca/uhn/fhir/util/HapiExtensions.java | 8 +- .../java/ca/uhn/fhir/util/ParametersUtil.java | 63 + .../fhir/validation/SchemaBaseValidator.java | 7 + hapi-fhir-bom/pom.xml | 9 +- hapi-fhir-cli/hapi-fhir-cli-api/pom.xml | 2 +- hapi-fhir-cli/hapi-fhir-cli-app/pom.xml | 2 +- hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml | 2 +- hapi-fhir-cli/pom.xml | 2 +- hapi-fhir-client-okhttp/pom.xml | 2 +- hapi-fhir-client/pom.xml | 2 +- .../client/method/OperationMethodBinding.java | 6 +- .../client/method/SearchMethodBinding.java | 11 +- hapi-fhir-converter/pom.xml | 2 +- hapi-fhir-dist/pom.xml | 2 +- hapi-fhir-docs/pom.xml | 15 +- .../CreateCompositionAndGenerateDocument.java | 73 + .../uhn/hapi/fhir/docs/ServletExamples.java | 19 + .../changelog/5_4_0/2560-openapi-support.yaml | 5 + .../ca/uhn/hapi/fhir/docs/client/examples.md | 10 + .../ca/uhn/hapi/fhir/docs/files.properties | 1 + .../built_in_server_interceptors.md | 5 + .../hapi/fhir/docs/server_plain/openapi.md | 39 + hapi-fhir-jacoco/pom.xml | 7 +- hapi-fhir-jaxrsserver-base/pom.xml | 2 +- hapi-fhir-jaxrsserver-example/pom.xml | 2 +- hapi-fhir-jpaserver-api/pom.xml | 2 +- hapi-fhir-jpaserver-base/pom.xml | 11 +- .../fhir/jpa/provider/BaseJpaProvider.java | 16 +- .../jpa/provider/BaseJpaResourceProvider.java | 151 +- .../jpa/provider/BaseJpaSystemProvider.java | 22 +- .../BaseJpaSystemProviderDstu2Plus.java | 37 +- .../uhn/fhir/jpa/provider/DiffProvider.java | 39 +- .../fhir/jpa/provider/GraphQLProvider.java | 15 +- .../JpaCapabilityStatementProvider.java | 5 + .../provider/JpaResourceProviderDstu2.java | 125 - .../jpa/provider/JpaSystemProviderDstu2.java | 62 - .../dstu3/JpaResourceProviderDstu3.java | 132 - .../dstu3/JpaSystemProviderDstu3.java | 68 - .../provider/r4/JpaResourceProviderR4.java | 124 - .../jpa/provider/r4/JpaSystemProviderR4.java | 64 - .../provider/r5/JpaResourceProviderR5.java | 126 - .../jpa/provider/r5/JpaSystemProviderR5.java | 54 - .../jpa/dao/r4/PartitioningSqlR4Test.java | 36 +- .../jpa/provider/SystemProviderDstu2Test.java | 5 +- .../jpa/provider/dstu3/ServerDstu3Test.java | 23 +- .../r4/OpenApiInterceptorJpaTest.java | 48 + ...rCapabilityStatementProviderJpaR4Test.java | 34 + .../jpa/provider/r4/SystemProviderR4Test.java | 23 +- ...lasticsearchSvcMultipleObservationsIT.java | 26 +- .../stresstest/GiantTransactionPerfTest.java | 2 - .../jpa/term/BaseTermReadSvcImplTest.java | 2 +- hapi-fhir-jpaserver-batch/pom.xml | 2 +- hapi-fhir-jpaserver-cql/pom.xml | 11 +- .../common/helper/TranslatorHelperTest.java | 34 +- hapi-fhir-jpaserver-mdm/pom.xml | 6 +- hapi-fhir-jpaserver-migrate/pom.xml | 2 +- hapi-fhir-jpaserver-model/pom.xml | 2 +- hapi-fhir-jpaserver-searchparam/pom.xml | 2 +- hapi-fhir-jpaserver-subscription/pom.xml | 2 +- hapi-fhir-jpaserver-test-utilities/pom.xml | 2 +- hapi-fhir-jpaserver-uhnfhirtest/pom.xml | 9 +- .../ca/uhn/fhirtest/TestRestfulServer.java | 17 +- .../uhn/fhirtest/config/TestDstu2Config.java | 2 +- .../uhn/fhirtest/config/TestDstu3Config.java | 2 +- .../ca/uhn/fhirtest/config/TestR4Config.java | 2 +- .../ca/uhn/fhirtest/config/TestR5Config.java | 2 +- .../src/main/webapp/WEB-INF/web.xml | 49 - hapi-fhir-server-mdm/pom.xml | 2 +- hapi-fhir-server-openapi/pom.xml | 78 + .../fhir/rest/openapi/OpenApiInterceptor.java | 903 ++++++ .../ca/uhn/fhir/rest/openapi/index.css | 141 + .../ca/uhn/fhir/rest/openapi/index.html | 66 + .../ca/uhn/fhir/rest/openapi/raccoon.png | Bin 0 -> 83876 bytes .../rest/openapi/OpenApiInterceptorTest.java | 274 ++ ...eptorWithAuthorizationInterceptorTest.java | 129 + .../src/test/resources/logback-test.xml | 16 + hapi-fhir-server/pom.xml | 2 +- .../fhir/rest/api/server/RequestDetails.java | 34 +- .../ca/uhn/fhir/rest/server/Bindings.java | 18 +- .../server/IServerConformanceProvider.java | 8 + .../uhn/fhir/rest/server/RestfulServer.java | 50 +- .../server/RestfulServerConfiguration.java | 230 +- .../rest/server/method/BaseMethodBinding.java | 28 +- .../BaseResourceReturningMethodBinding.java | 17 +- .../method/ConformanceMethodBinding.java | 11 + .../server/method/GraphQLMethodBinding.java | 28 +- .../fhir/rest/server/method/MethodUtil.java | 38 +- .../server/method/OperationMethodBinding.java | 57 +- .../server/method/OperationParameter.java | 25 +- .../server/method/SearchMethodBinding.java | 11 +- .../ValidateMethodBindingDstu2Plus.java | 11 +- .../ServerCapabilityStatementProvider.java | 1397 +++++---- .../server/servlet/ServletRequestDetails.java | 21 + .../method/SearchMethodBindingTest.java | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../hapi-fhir-spring-boot-samples/pom.xml | 2 +- .../hapi-fhir-spring-boot-starter/pom.xml | 2 +- hapi-fhir-spring-boot/pom.xml | 2 +- hapi-fhir-structures-dstu2.1/pom.xml | 2 +- .../server/ServerConformanceProvider.java | 6 +- .../server/OperationServerDstu2_1Test.java | 2 +- ...ServerWithSearchParamTypesDstu2_1Test.java | 10 +- hapi-fhir-structures-dstu2/pom.xml | 2 +- .../dstu2/ServerConformanceProvider.java | 8 +- .../uhn/fhir/parser/XmlParserDstu2Test.java | 4 +- .../OperationDuplicateServerDstu2Test.java | 26 +- .../rest/server/OperationServerDstu2Test.java | 4 +- ...onServerWithSearchParamTypesDstu2Test.java | 4 +- .../ServerConformanceProviderDstu2Test.java | 180 +- .../InterceptorUserDataMapDstu2Test.java | 3 +- hapi-fhir-structures-dstu3/pom.xml | 2 +- .../ServerCapabilityStatementProvider.java | 8 +- .../rest/server/OperationServerDstu3Test.java | 10 +- ...onServerWithSearchParamTypesDstu3Test.java | 13 +- .../ca/uhn/fhir/util/XmlUtilDstu3Test.java | 6 +- ...rCapabilityStatementProviderDstu3Test.java | 81 +- hapi-fhir-structures-hl7org-dstu2/pom.xml | 2 +- .../server/ServerConformanceProvider.java | 6 +- ...erationDuplicateServerHl7OrgDstu2Test.java | 8 +- ...verConformanceProviderHl7OrgDstu2Test.java | 40 +- hapi-fhir-structures-r4/pom.xml | 2 +- .../rest/server/OperationServerR4Test.java | 49 +- .../fhir/rest/server/RestfulServerTest.java | 80 +- .../server/ServerInvalidDefinitionR4Test.java | 41 +- .../auth/AuthorizationInterceptorR4Test.java | 34 +- hapi-fhir-structures-r5/pom.xml | 2 +- ...rverCapabilityStatementProviderR5Test.java | 103 +- hapi-fhir-test-utilities/pom.xml | 35 +- .../ca/uhn/fhir/test/utilities/HtmlUtil.java | 56 + .../ca/uhn/fhir/test/utilities/JettyUtil.java | 19 +- .../utilities/server/MockServletUtil.java | 42 + .../server/RestfulServerExtension.java | 23 +- hapi-fhir-testpage-overlay/pom.xml | 2 +- .../pom.xml | 2 +- hapi-fhir-validation-resources-dstu2/pom.xml | 2 +- hapi-fhir-validation-resources-dstu3/pom.xml | 2 +- hapi-fhir-validation-resources-r4/pom.xml | 2 +- hapi-fhir-validation-resources-r5/pom.xml | 2 +- hapi-fhir-validation/pom.xml | 2 +- ...RequestValidatingInterceptorDstu3Test.java | 2 + ...rverCapabilityStatementProviderR4Test.java | 2594 +++++++++-------- .../validation/SchemaValidationDstu3Test.java | 4 + .../r4/validation/SchemaValidationR4Test.java | 65 +- hapi-tinder-plugin/pom.xml | 16 +- hapi-tinder-test/pom.xml | 2 +- pom.xml | 63 +- restful-server-example/pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- 170 files changed, 5693 insertions(+), 3485 deletions(-) create mode 100644 hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/CreateCompositionAndGenerateDocument.java create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2560-openapi-support.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/openapi.md create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/OpenApiInterceptorJpaTest.java create mode 100644 hapi-fhir-server-openapi/pom.xml create mode 100644 hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java create mode 100644 hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.css create mode 100644 hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.html create mode 100644 hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/raccoon.png create mode 100644 hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorTest.java create mode 100644 hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorWithAuthorizationInterceptorTest.java create mode 100644 hapi-fhir-server-openapi/src/test/resources/logback-test.xml rename {hapi-fhir-server => hapi-fhir-structures-r4}/src/test/java/ca/uhn/fhir/rest/server/RestfulServerTest.java (67%) create mode 100644 hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/HtmlUtil.java create mode 100644 hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/MockServletUtil.java diff --git a/hapi-deployable-pom/pom.xml b/hapi-deployable-pom/pom.xml index 9df4f52f5df..e5c236da510 100644 --- a/hapi-deployable-pom/pom.xml +++ b/hapi-deployable-pom/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index a03a6465c2a..29daf3ce599 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 912fe4efd95..d841d0053e9 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeDeclaredChildDefinition.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeDeclaredChildDefinition.java index 3008324e443..e0dc2a4dceb 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeDeclaredChildDefinition.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/BaseRuntimeDeclaredChildDefinition.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.context; import ca.uhn.fhir.model.api.annotation.Child; import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.ValidateUtil; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; @@ -60,7 +61,7 @@ public abstract class BaseRuntimeDeclaredChildDefinition extends BaseRuntimeChil myElementName = theElementName; if (theDescriptionAnnotation != null) { myShortDefinition = theDescriptionAnnotation.shortDefinition(); - myFormalDefinition = theDescriptionAnnotation.formalDefinition(); + myFormalDefinition = ParametersUtil.extractDescription(theDescriptionAnnotation); } else { myShortDefinition = null; myFormalDefinition = null; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Description.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Description.java index 21cc111e9c6..ecb5317a5f6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Description.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/annotation/Description.java @@ -30,17 +30,33 @@ import java.lang.annotation.Target; * a search parameter definition in order to provide documentation for that item. */ @Retention(RetentionPolicy.RUNTIME) -@Target(value= {ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER, ElementType.METHOD}) +@Target(value = {ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER, ElementType.METHOD}) public @interface Description { /** - * Optional short name for this child + * A description of this method or parameter + * + * @since 5.4.0 + */ + String value() default ""; + + /** + * Optional short name for this child */ String shortDefinition() default ""; - + /** * Optional formal definition for this child + * + * @deprecated Use {@link #value()} instead. Deprecated in 5.4.0. */ + @Deprecated String formalDefinition() default ""; - + + /** + * May be used to supply example values for this + * + * @since 5.4.0 + */ + String[] example() default {}; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/AddTags.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/AddTags.java index efb3959e40f..00999343985 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/AddTags.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/AddTags.java @@ -78,4 +78,13 @@ public @interface AddTags { */ Class type() default IBaseResource.class; + /** + * This method allows the return type for this method to be specified in a + * non-type-specific way, using the text name of the resource, e.g. "Patient". + * + * This attribute should be populate, or {@link #type()} should be, but not both. + * + * @since 5.4.0 + */ + String typeName() default ""; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Create.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Create.java index 2d0ab2811b9..f00f6af8e59 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Create.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Create.java @@ -45,6 +45,15 @@ public @interface Create { */ // NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere Class type() default IBaseResource.class; - + + /** + * This method allows the return type for this method to be specified in a + * non-type-specific way, using the text name of the resource, e.g. "Patient". + * + * This attribute should be populate, or {@link #type()} should be, but not both. + * + * @since 5.4.0 + */ + String typeName() default ""; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Delete.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Delete.java index 33985849f09..82d7e4f1ac8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Delete.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Delete.java @@ -47,5 +47,16 @@ public @interface Delete { * for client implementations. */ // NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere - Class type() default IBaseResource.class; + Class type() default IBaseResource.class; + + /** + * This method allows the return type for this method to be specified in a + * non-type-specific way, using the text name of the resource, e.g. "Patient". + * + * This attribute should be populate, or {@link #type()} should be, but not both. + * + * @since 5.4.0 + */ + String typeName() default ""; + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/DeleteTags.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/DeleteTags.java index cfed68e45cb..202e82f7c14 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/DeleteTags.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/DeleteTags.java @@ -74,4 +74,14 @@ public @interface DeleteTags { */ Class type() default IBaseResource.class; + /** + * This method allows the return type for this method to be specified in a + * non-type-specific way, using the text name of the resource, e.g. "Patient". + * + * This attribute should be populate, or {@link #type()} should be, but not both. + * + * @since 5.4.0 + */ + String typeName() default ""; + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/History.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/History.java index 6f43678de2f..cd2e0591838 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/History.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/History.java @@ -80,5 +80,15 @@ public @interface History { * for information on usage patterns. */ Class type() default IBaseResource.class; - + + /** + * This method allows the return type for this method to be specified in a + * non-type-specific way, using the text name of the resource, e.g. "Patient". + * + * This attribute should be populate, or {@link #type()} should be, but not both. + * + * @since 5.4.0 + */ + String typeName() default ""; + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java index 05f2b59f176..d7b1129dd33 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Operation.java @@ -132,7 +132,10 @@ public @interface Operation { /** * If this is set to true, this method will be a global operation - * meaning that it applies to all resource types + * meaning that it applies to all resource types. Operations with this flag set should be + * placed in Plain Providers (i.e. they don't need to be placed in a resource-type-specific + * IResourceProvider instance) and should have a parameter annotated with + * {@link IdParam}. */ boolean global() default false; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java index f807cb10613..6a9af947295 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/OperationParam.java @@ -39,7 +39,7 @@ public @interface OperationParam { /** * Value for {@link OperationParam#max()} indicating no maximum */ - final int MAX_UNLIMITED = -1; + int MAX_UNLIMITED = -1; /** @@ -57,7 +57,7 @@ public @interface OperationParam { * * @since 1.5 */ - final int MAX_DEFAULT = -2; + int MAX_DEFAULT = -2; /** * The name of the parameter diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Patch.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Patch.java index 25db4fc6b3b..2e788cba624 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Patch.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Patch.java @@ -50,4 +50,13 @@ public @interface Patch { // NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere Class type() default IBaseResource.class; + /** + * This method allows the return type for this method to be specified in a + * non-type-specific way, using the text name of the resource, e.g. "Patient". + * + * This attribute should be populate, or {@link #type()} should be, but not both. + * + * @since 5.4.0 + */ + String typeName() default ""; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java index 7f7b0b3799b..3ba92da4e75 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Search.java @@ -78,6 +78,16 @@ public @interface Search { // NB: Read, Search (maybe others) share this annotation method, so update the javadocs everywhere Class type() default IBaseResource.class; + /** + * This method allows the return type for this method to be specified in a + * non-type-specific way, using the text name of the resource, e.g. "Patient". + * + * This attribute should be populate, or {@link #type()} should be, but not both. + * + * @since 5.4.0 + */ + String typeName() default ""; + /** * In a REST server, should this method be invoked even if it does not have method parameters * which correspond to all of the URL parameters passed in by the client (default is false). @@ -91,4 +101,5 @@ public @interface Search { *

    */ boolean allowUnknownParams() default false; + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Update.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Update.java index 4392f5d6df5..763f44c8956 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Update.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Update.java @@ -44,9 +44,19 @@ public @interface Update { * The return type for this search method. This generally does not need * to be populated for a server implementation, since servers will return * only one resource per class, but generally does need to be populated - * for client implementations. + * for client implementations. */ // NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere Class type() default IResource.class; - + + /** + * This method allows the return type for this method to be specified in a + * non-type-specific way, using the text name of the resource, e.g. "Patient". + *

    + * This attribute should be populate, or {@link #type()} should be, but not both. + * + * @since 5.4.0 + */ + String typeName() default ""; + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Validate.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Validate.java index 28b675544da..434700d90bc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Validate.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Validate.java @@ -52,7 +52,17 @@ public @interface Validate { */ // NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere Class type() default IBaseResource.class; - + + /** + * This method allows the return type for this method to be specified in a + * non-type-specific way, using the text name of the resource, e.g. "Patient". + * + * This attribute should be populate, or {@link #type()} should be, but not both. + * + * @since 5.4.0 + */ + String typeName() default ""; + /** * Validation mode parameter annotation for the validation mode parameter (only supported * in FHIR DSTU2+). Parameter must be of type {@link ValidationModeEnum}. diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java index 95b08d8ceb8..e14667a1af3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ExtensionUtil.java @@ -36,6 +36,13 @@ import java.util.stream.Collectors; */ public class ExtensionUtil { + /** + * Non instantiable + */ + private ExtensionUtil() { + // nothing + } + /** * Returns an extension with the specified URL creating one if it doesn't exist. * @@ -46,7 +53,7 @@ public class ExtensionUtil { */ public static IBaseExtension getOrCreateExtension(IBase theBase, String theUrl) { IBaseHasExtensions baseHasExtensions = validateExtensionSupport(theBase); - IBaseExtension extension = getExtensionByUrl(baseHasExtensions, theUrl); + IBaseExtension extension = getExtensionByUrl(baseHasExtensions, theUrl); if (extension == null) { extension = baseHasExtensions.addExtension(); extension.setUrl(theUrl); @@ -75,13 +82,27 @@ public class ExtensionUtil { */ public static IBaseExtension addExtension(IBase theBase, String theUrl) { IBaseHasExtensions baseHasExtensions = validateExtensionSupport(theBase); - IBaseExtension extension = baseHasExtensions.addExtension(); + IBaseExtension extension = baseHasExtensions.addExtension(); if (theUrl != null) { extension.setUrl(theUrl); } return extension; } + /** + * Adds an extension with the specified value + * + * @param theBase The resource to update extension on + * @param theUrl Extension URL + * @param theValueType Type of the value to set in the extension + * @param theValue Extension value + * @param theFhirContext The context containing FHIR resource definitions + */ + public static void addExtension(FhirContext theFhirContext, IBase theBase, String theUrl, String theValueType, Object theValue) { + IBaseExtension ext = addExtension(theBase, theUrl); + setExtension(theFhirContext, ext, theValueType, theValue); + } + private static IBaseHasExtensions validateExtensionSupport(IBase theBase) { if (!(theBase instanceof IBaseHasExtensions)) { throw new IllegalArgumentException(String.format("Expected instance that supports extensions, but got %s", theBase)); @@ -118,7 +139,7 @@ public class ExtensionUtil { if (!hasExtension(theBase, theExtensionUrl)) { return false; } - IBaseDatatype value = getExtensionByUrl((IBaseHasExtensions) theBase, theExtensionUrl).getValue(); + IBaseDatatype value = getExtensionByUrl(theBase, theExtensionUrl).getValue(); if (value == null) { return theExtensionValue == null; } @@ -133,7 +154,7 @@ public class ExtensionUtil { * @return Returns the first available extension with the specified URL, or null if such extension doesn't exist */ public static IBaseExtension getExtensionByUrl(IBase theBase, String theExtensionUrl) { - Predicate filter; + Predicate> filter; if (theExtensionUrl == null) { filter = (e -> true); } else { @@ -153,7 +174,7 @@ public class ExtensionUtil { * @param theFilter Predicate to match the extension against * @return Returns all extension with the specified URL, or an empty list if such extensions do not exist */ - public static List> getExtensionsMatchingPredicate(IBase theBase, Predicate theFilter) { + public static List> getExtensionsMatchingPredicate(IBase theBase, Predicate> theFilter) { return validateExtensionSupport(theBase) .getExtension() .stream() @@ -189,7 +210,7 @@ public class ExtensionUtil { * @param theFilter Defines which extensions should be cleared * @return Returns all extension that were removed */ - private static List> clearExtensionsMatchingPredicate(IBase theBase, Predicate theFilter) { + private static List> clearExtensionsMatchingPredicate(IBase theBase, Predicate> theFilter) { List> retVal = getExtensionsMatchingPredicate(theBase, theFilter); validateExtensionSupport(theBase) .getExtension() @@ -205,7 +226,7 @@ public class ExtensionUtil { * @return Returns all extension with the specified URL, or an empty list if such extensions do not exist */ public static List> getExtensionsByUrl(IBaseHasExtensions theBase, String theExtensionUrl) { - Predicate urlEqualityPredicate = e -> theExtensionUrl.equals(e.getUrl()); + Predicate> urlEqualityPredicate = e -> theExtensionUrl.equals(e.getUrl()); return getExtensionsMatchingPredicate(theBase, urlEqualityPredicate); } @@ -216,8 +237,8 @@ public class ExtensionUtil { * @param theValue The value to set * @param theFhirContext The context containing FHIR resource definitions */ - public static void setExtension(FhirContext theFhirContext, IBaseExtension theExtension, String theValue) { - setExtension(theFhirContext, theExtension, "string", (Object) theValue); + public static void setExtension(FhirContext theFhirContext, IBaseExtension theExtension, String theValue) { + setExtension(theFhirContext, theExtension, "string", theValue); } /** @@ -228,7 +249,7 @@ public class ExtensionUtil { * @param theValue The value to set * @param theFhirContext The context containing FHIR resource definitions */ - public static void setExtension(FhirContext theFhirContext, IBaseExtension theExtension, String theExtensionType, Object theValue) { + public static void setExtension(FhirContext theFhirContext, IBaseExtension theExtension, String theExtensionType, Object theValue) { theExtension.setValue(TerserUtil.newElement(theFhirContext, theExtensionType, theValue)); } @@ -241,7 +262,7 @@ public class ExtensionUtil { * @param theFhirContext The context containing FHIR resource definitions */ public static void setExtensionAsString(FhirContext theFhirContext, IBase theBase, String theUrl, String theValue) { - IBaseExtension ext = getOrCreateExtension(theBase, theUrl); + IBaseExtension ext = getOrCreateExtension(theBase, theUrl); setExtension(theFhirContext, ext, theValue); } @@ -255,7 +276,7 @@ public class ExtensionUtil { * @param theFhirContext The context containing FHIR resource definitions */ public static void setExtension(FhirContext theFhirContext, IBase theBase, String theUrl, String theValueType, Object theValue) { - IBaseExtension ext = getOrCreateExtension(theBase, theUrl); + IBaseExtension ext = getOrCreateExtension(theBase, theUrl); setExtension(theFhirContext, ext, theValueType, theValue); } @@ -266,7 +287,7 @@ public class ExtensionUtil { * @param theRightExtension : Extension to be evaluated #2 * @return Result of the comparison */ - public static boolean equals(IBaseExtension theLeftExtension, IBaseExtension theRightExtension) { + public static boolean equals(IBaseExtension theLeftExtension, IBaseExtension theRightExtension) { return TerserUtil.equals(theLeftExtension, theRightExtension); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/HapiExtensions.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/HapiExtensions.java index f2cf7903407..6dfdca94d4b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/HapiExtensions.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/HapiExtensions.java @@ -116,7 +116,13 @@ public class HapiExtensions { */ public static final String ASSOCIATED_GOLDEN_RESOURCE_EXTENSION_URL = "https://hapifhir.org/associated-patient-golden-resource/"; - /** + /** + * This extension provides an example value for a parameter value for + * a REST operation (eg for an OperationDefinition) + */ + public static final String EXT_OP_PARAMETER_EXAMPLE_VALUE = "http://hapifhir.io/fhir/StructureDefinition/op-parameter-example-value"; + + /** * Non instantiable */ private HapiExtensions() { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java index 40750ba0bef..5c873c94114 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ParametersUtil.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; import ca.uhn.fhir.context.BaseRuntimeElementDefinition; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.primitive.StringDt; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; @@ -34,8 +35,13 @@ import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import javax.annotation.Nullable; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -43,6 +49,7 @@ import java.util.function.Function; import java.util.stream.Collectors; import static org.apache.commons.lang3.StringUtils.defaultIfBlank; +import static org.apache.commons.lang3.StringUtils.isBlank; /** * Utilities for dealing with parameters resources in a version indepenedent way @@ -418,4 +425,60 @@ public class ParametersUtil { .findFirst(); } + @Nullable + public static String extractDescription(AnnotatedElement theType) { + Description description = theType.getAnnotation(Description.class); + if (description != null) { + return extractDescription(description); + } else { + return null; + } + } + + @Nullable + public static String extractDescription(Description desc) { + String description = desc.value(); + if (isBlank(description)) { + description = desc.formalDefinition(); + } + if (isBlank(description)) { + description = desc.shortDefinition(); + } + return defaultIfBlank(description, null); + } + + @Nullable + public static String extractShortDefinition(AnnotatedElement theType) { + Description description = theType.getAnnotation(Description.class); + if (description != null) { + return defaultIfBlank(description.shortDefinition(), null); + } else { + return null; + } + } + + public static String extractDescription(Annotation[] theParameterAnnotations) { + for (Annotation next : theParameterAnnotations) { + if (next instanceof Description) { + return extractDescription((Description)next); + } + } + return null; + } + + public static List extractExamples(Annotation[] theParameterAnnotations) { + ArrayList retVal = null; + for (Annotation next : theParameterAnnotations) { + if (next instanceof Description) { + String[] examples = ((Description) next).example(); + if (examples.length > 0) { + if (retVal == null) { + retVal = new ArrayList<>(); + } + retVal.addAll(Arrays.asList(examples)); + } + } + } + return retVal; + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java index c5b1b9832b3..f71d6e4c4a0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/SchemaBaseValidator.java @@ -51,6 +51,7 @@ public class SchemaBaseValidator implements IValidatorModule { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaBaseValidator.class); private static final Set SCHEMA_NAMES; + private static boolean ourJaxp15Supported; static { HashSet sn = new HashSet<>(); @@ -132,7 +133,9 @@ public class SchemaBaseValidator implements IValidatorModule { * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing */ schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + ourJaxp15Supported = true; } catch (SAXNotRecognizedException e) { + ourJaxp15Supported = false; ourLog.warn("Jaxp 1.5 Support not found.", e); } schema = schemaFactory.newSchema(new Source[]{baseSource}); @@ -216,4 +219,8 @@ public class SchemaBaseValidator implements IValidatorModule { } + public static boolean isJaxp15Supported() { + return ourJaxp15Supported; + } + } diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml index fdb411e91dc..e069462596a 100644 --- a/hapi-fhir-bom/pom.xml +++ b/hapi-fhir-bom/pom.xml @@ -3,14 +3,14 @@ 4.0.0 ca.uhn.hapi.fhir hapi-fhir-bom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT pom HAPI FHIR BOM ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -41,6 +41,11 @@ hapi-fhir-server-mdm ${project.version} + + ${project.groupId} + hapi-fhir-server-openapi + ${project.version} + ${project.groupId} hapi-fhir-validation diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-api/pom.xml index 92bafa40399..175508dea4a 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-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 0c0e9694712..ee4cd6101ed 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml index 23fdc156c0e..6735fe66129 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml +++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../../hapi-deployable-pom diff --git a/hapi-fhir-cli/pom.xml b/hapi-fhir-cli/pom.xml index ae67b1dad2f..377ac7c1d20 100644 --- a/hapi-fhir-cli/pom.xml +++ b/hapi-fhir-cli/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-client-okhttp/pom.xml b/hapi-fhir-client-okhttp/pom.xml index 52fed03fb71..4656c910de6 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/pom.xml b/hapi-fhir-client/pom.xml index f407f56be2d..d86715e8679 100644 --- a/hapi-fhir-client/pom.xml +++ b/hapi-fhir-client/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/OperationMethodBinding.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/OperationMethodBinding.java index 9e5f4c100da..eb17cbb2241 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/OperationMethodBinding.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/OperationMethodBinding.java @@ -30,6 +30,7 @@ import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation; import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.ParametersUtil; import org.hl7.fhir.instance.model.api.*; import java.lang.reflect.Method; @@ -64,10 +65,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { Description description = theMethod.getAnnotation(Description.class); if (description != null) { - myDescription = description.formalDefinition(); - if (isBlank(myDescription)) { - myDescription = description.shortDefinition(); - } + myDescription = ParametersUtil.extractDescription(description); } if (isBlank(myDescription)) { myDescription = null; diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchMethodBinding.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchMethodBinding.java index 1e19ab8abe5..7341e3f6fa4 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchMethodBinding.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/method/SearchMethodBinding.java @@ -33,6 +33,7 @@ import ca.uhn.fhir.rest.client.impl.BaseHttpClientInvocation; import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.util.ParametersUtil; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -59,15 +60,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { this.myQueryName = StringUtils.defaultIfBlank(search.queryName(), null); this.myCompartmentName = StringUtils.defaultIfBlank(search.compartmentName(), null); this.myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); - - Description desc = theMethod.getAnnotation(Description.class); - if (desc != null) { - if (isNotBlank(desc.formalDefinition())) { - myDescription = StringUtils.defaultIfBlank(desc.formalDefinition(), null); - } else { - myDescription = StringUtils.defaultIfBlank(desc.shortDefinition(), null); - } - } + this.myDescription = ParametersUtil.extractDescription(theMethod); /* * Check for parameter combinations and names that are invalid diff --git a/hapi-fhir-converter/pom.xml b/hapi-fhir-converter/pom.xml index a988e2f45c5..7778e520ac6 100644 --- a/hapi-fhir-converter/pom.xml +++ b/hapi-fhir-converter/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-dist/pom.xml b/hapi-fhir-dist/pom.xml index 8ea8d57f03b..95e10af58f2 100644 --- a/hapi-fhir-dist/pom.xml +++ b/hapi-fhir-dist/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml index 2fcd0c94674..d15eda3ec2e 100644 --- a/hapi-fhir-docs/pom.xml +++ b/hapi-fhir-docs/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -55,6 +55,11 @@ hapi-fhir-jpaserver-base ${project.version} + + ca.uhn.hapi.fhir + hapi-fhir-server-openapi + ${project.version} + com.fasterxml.jackson.dataformat @@ -78,13 +83,13 @@ ca.uhn.hapi.fhir hapi-fhir-structures-dstu2 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT compile ca.uhn.hapi.fhir hapi-fhir-jpaserver-subscription - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT compile @@ -101,7 +106,7 @@ ca.uhn.hapi.fhir hapi-fhir-testpage-overlay - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT classes @@ -118,7 +123,7 @@ com.fasterxml.jackson.core jackson-databind - + diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/CreateCompositionAndGenerateDocument.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/CreateCompositionAndGenerateDocument.java new file mode 100644 index 00000000000..546bf051f39 --- /dev/null +++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/CreateCompositionAndGenerateDocument.java @@ -0,0 +1,73 @@ +package ca.uhn.hapi.fhir.docs; + +/*- + * #%L + * HAPI FHIR - Docs + * %% + * Copyright (C) 2014 - 2021 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.FhirContext; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Composition; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CreateCompositionAndGenerateDocument { + + private static final Logger ourLog = LoggerFactory.getLogger(CreateCompositionAndGenerateDocument.class); + + public static void main(String[] args) { + + // START SNIPPET: CreateCompositionAndGenerateDocument + FhirContext ctx = FhirContext.forR4(); + IGenericClient client = ctx.newRestfulGenericClient("http://hapi.fhir.org/baseR4"); + + Patient patient = new Patient(); + patient.setId("PATIENT-ABC"); + patient.setActive(true); + client.update().resource(patient).execute(); + + Observation observation = new Observation(); + observation.setId("OBSERVATION-ABC"); + observation.setSubject(new Reference("Patient/PATIENT-ABC")); + observation.setStatus(Observation.ObservationStatus.FINAL); + client.update().resource(observation).execute(); + + Composition composition = new Composition(); + composition.setId("COMPOSITION-ABC"); + composition.setSubject(new Reference("Patient/PATIENT-ABC")); + composition.addSection().setFocus(new Reference("Observation/OBSERVATION-ABC")); + client.update().resource(composition).execute(); + + Bundle document = client + .operation() + .onInstance("Composition/COMPOSITION-ABC") + .named("$document") + .withNoParameters(Parameters.class) + .returnResourceType(Bundle.class) + .execute(); + + ourLog.info("Document bundle: {}", ctx.newJsonParser().setPrettyPrint(true).encodeResourceToString(document)); + // END SNIPPET: CreateCompositionAndGenerateDocument + + } +} diff --git a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ServletExamples.java b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ServletExamples.java index e682175e0fb..9ed097ad9ed 100644 --- a/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ServletExamples.java +++ b/hapi-fhir-docs/src/main/java/ca/uhn/hapi/fhir/docs/ServletExamples.java @@ -23,6 +23,7 @@ package ca.uhn.hapi.fhir.docs; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.support.IValidationSupport; import ca.uhn.fhir.rest.api.PreferHandlingEnum; +import ca.uhn.fhir.rest.openapi.OpenApiInterceptor; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.interceptor.*; @@ -65,6 +66,24 @@ public class ServletExamples { } // END SNIPPET: loggingInterceptor + // START SNIPPET: OpenApiInterceptor + @WebServlet(urlPatterns = { "/fhir/*" }, displayName = "FHIR Server") + public class RestfulServerWithOpenApi extends RestfulServer { + + @Override + protected void initialize() throws ServletException { + + // ... define your resource providers here ... + + // Now register the interceptor + OpenApiInterceptor openApiInterceptor = new OpenApiInterceptor(); + registerInterceptor(openApiInterceptor); + + } + + } + // END SNIPPET: OpenApiInterceptor + // START SNIPPET: validatingInterceptor @WebServlet(urlPatterns = { "/fhir/*" }, displayName = "FHIR Server") public class ValidatingServerWithLogging extends RestfulServer { diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2560-openapi-support.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2560-openapi-support.yaml new file mode 100644 index 00000000000..cfd9b53dc35 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2560-openapi-support.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 2560 +title: "A new interceptor called `OpenApiInterceptor` has been added. This interceptor can be registered against FHIR Servers to + automatically add support for OpenAPI / Swagger." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/client/examples.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/client/examples.md index e3267ffed28..9a6c405390d 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/client/examples.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/client/examples.md @@ -161,3 +161,13 @@ This following example shows how to load all pages of a bundle by fetching each ```java {{snippet:classpath:/ca/uhn/hapi/fhir/docs/BundleFetcher.java|loadAll}} ``` + +# Create Composition and Generate Document + +This example shows how to generate a Composition resource with two linked resources, then apply the server `$document` operation to generate a document based on this composition. + +```java +{{snippet:classpath:/ca/uhn/hapi/fhir/docs/CreateCompositionAndGenerateDocument.java|CreateCompositionAndGenerateDocument}} +``` + + diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties index 580cda79968..dac40d5f791 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/files.properties @@ -47,6 +47,7 @@ page.server_plain.web_testpage_overlay=Web Testpage Overlay page.server_plain.multitenancy=Multitenancy page.server_plain.jax_rs=JAX-RS Support page.server_plain.customizing_the_capabilitystatement=Customizing the CapabilityStatement +page.server_plain.openapi=OpenAPI / Swagger section.server_jpa.title=JPA Server page.server_jpa.introduction=Introduction diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md index f21e80292ec..2f6ecef421d 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/interceptors/built_in_server_interceptors.md @@ -196,6 +196,11 @@ Some security audit tools require that servers return an HTTP 405 if an unsuppor * [BanUnsupportedHttpMethodsInterceptor JavaDoc](/apidocs/hapi-fhir-server/ca/uhn/fhir/rest/server/interceptor/BanUnsupportedHttpMethodsInterceptor.html) * [BanUnsupportedHttpMethodsInterceptor Source](https://github.com/hapifhir/hapi-fhir/blob/master/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/BanUnsupportedHttpMethodsInterceptor.java) +# Server: OpenAPI / Swagger Support + +An interceptor can be registered against your server that enables support for OpenAPI (aka Swagger) automatically. See [OpenAPI](/docs/server_plain/openapi.html) for more information. + + # Subscription: Subscription Debug Log Interceptor When using Subscriptions, the debug log interceptor can be used to add a number of additional lines to the server logs showing the internals of the subscription processing pipeline. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/openapi.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/openapi.md new file mode 100644 index 00000000000..4f8a7f040ff --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/docs/server_plain/openapi.md @@ -0,0 +1,39 @@ +# OpenAPI / Swagger Support + +In HAPI FHIR, support for OpenAPI (aka Swagger) is supported via the [OpenApiInterceptor](/hapi-fhir/apidocs/hapi-fhir-server-openapi/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.html). + +Note that this interceptor supports servers using the RestfulServer (aka HAPI FHIR Plain Server and JPA Server), and does not currently support JAX-RS servers. + +When this interceptor is registered against the server, it performs the following 3 tasks: + +### System Functionality + +* OpenAPI 3.0 Documentation will be served at `[baseUrl]/api-docs`. This documentation is generated by the interceptor using information from the server's CapabilityStatement as well as from its automatically generated OperationDefinitions. + +### User Functionality + +* Anytime a user using a browser navigates to the Base URL of the server, they will be automatically redirected to `[baseUrl]/swagger-ui/` + +* A customized version of the [Swagger UI](https://swagger.io/tools/swagger-ui/) tool will be served at `[baseUrl]/swagger-ui/` + +# Enabling OpenAPI + +The HAPI FHIR OpenAPI functionality is supplied in a dedicated module called `hapi-fhir-server-openapi`. To enable this functionality you must first include this module in your project. For example, Maven users should include the following dependency: + +```xml + + ca.uhn.hapi.fhir + hapi-fhir-server-openapi + VERSION + +``` + +You then simply have to register the interceptor against your RestfulServer instance. + +```java +{{snippet:classpath:/ca/uhn/hapi/fhir/docs/ServletExamples.java|OpenApiInterceptor}} +``` + +# Demonstration + +See the HAPI FHIR Test Server for a demonstration of HAPI FHIR OpenAPI functionality: http://hapi.fhir.org/baseR4/swagger-ui/ diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index 1d22f0b5e4a..6af26480e26 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -11,7 +11,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -36,6 +36,11 @@ hapi-fhir-server-mdm ${project.version} + + ca.uhn.hapi.fhir + hapi-fhir-server-openapi + ${project.version} + ca.uhn.hapi.fhir hapi-fhir-client diff --git a/hapi-fhir-jaxrsserver-base/pom.xml b/hapi-fhir-jaxrsserver-base/pom.xml index b2004c3ad14..697e4479704 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jaxrsserver-example/pom.xml b/hapi-fhir-jaxrsserver-example/pom.xml index c3417a5963f..6626a7653dc 100644 --- a/hapi-fhir-jaxrsserver-example/pom.xml +++ b/hapi-fhir-jaxrsserver-example/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-jpaserver-api/pom.xml b/hapi-fhir-jpaserver-api/pom.xml index fa591624b35..35aecfe7055 100644 --- a/hapi-fhir-jpaserver-api/pom.xml +++ b/hapi-fhir-jpaserver-api/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index 3fed26d92f7..357583621aa 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -577,7 +577,14 @@ test - + + ca.uhn.hapi.fhir + hapi-fhir-server-openapi + ${project.version} + test + + + com.github.ben-manes.caffeine caffeine diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java index 2b4cf56a725..4f772f60abf 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaProvider.java @@ -8,10 +8,10 @@ import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.util.ParametersUtil; import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r4.model.IntegerType; -import org.hl7.fhir.r4.model.Parameters; import org.jboss.logging.MDC; import org.springframework.beans.factory.annotation.Autowired; @@ -78,13 +78,11 @@ public class BaseJpaProvider { return options; } - protected Parameters createExpungeResponse(ExpungeOutcome theOutcome) { - Parameters retVal = new Parameters(); - retVal - .addParameter() - .setName(JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT) - .setValue(new IntegerType(theOutcome.getDeletedCount())); - return retVal; + protected IBaseParameters createExpungeResponse(ExpungeOutcome theOutcome) { + IBaseParameters parameters = ParametersUtil.newInstance(getContext()); + String value = Integer.toString(theOutcome.getDeletedCount()); + ParametersUtil.addParameterToParameters(getContext(), parameters, JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, "integer", value); + return parameters; } /** diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProvider.java index 3d73fd326d8..307625c456a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaResourceProvider.java @@ -24,30 +24,47 @@ import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.api.model.ExpungeOptions; import ca.uhn.fhir.jpa.api.model.ExpungeOutcome; +import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.rest.annotation.At; import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; +import ca.uhn.fhir.rest.annotation.Create; +import ca.uhn.fhir.rest.annotation.Delete; import ca.uhn.fhir.rest.annotation.History; import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.Patch; import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.Since; +import ca.uhn.fhir.rest.annotation.Update; +import ca.uhn.fhir.rest.annotation.Validate; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.PatchTypeEnum; +import ca.uhn.fhir.rest.api.ValidationModeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.util.CoverageIgnore; +import ca.uhn.fhir.util.ParametersUtil; +import org.hl7.fhir.instance.model.api.IBaseMetaType; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r4.model.Parameters; import org.springframework.beans.factory.annotation.Required; import javax.servlet.http.HttpServletRequest; import java.util.Date; +import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_META; +import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_META_ADD; +import static ca.uhn.fhir.jpa.model.util.JpaConstants.OPERATION_META_DELETE; + public abstract class BaseJpaResourceProvider extends BaseJpaProvider implements IResourceProvider { private IFhirResourceDao myDao; @@ -62,7 +79,7 @@ public abstract class BaseJpaResourceProvider extends B } - protected Parameters doExpunge(IIdType theIdParam, IPrimitiveType theLimit, IPrimitiveType theExpungeDeletedResources, IPrimitiveType theExpungeOldVersions, IPrimitiveType theExpungeEverything, RequestDetails theRequest) { + protected IBaseParameters doExpunge(IIdType theIdParam, IPrimitiveType theLimit, IPrimitiveType theExpungeDeletedResources, IPrimitiveType theExpungeOldVersions, IPrimitiveType theExpungeEverything, RequestDetails theRequest) { ExpungeOptions options = createExpungeOptions(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything); @@ -143,4 +160,134 @@ public abstract class BaseJpaResourceProvider extends B } } + @Create + public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) { + startRequest(theRequest); + try { + if (theConditional != null) { + return getDao().create(theResource, theConditional, theRequestDetails); + } else { + return getDao().create(theResource, theRequestDetails); + } + } finally { + endRequest(theRequest); + } + } + + @Delete() + public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IIdType theResource, @ConditionalUrlParam(supportsMultiple = true) String theConditional, RequestDetails theRequestDetails) { + startRequest(theRequest); + try { + if (theConditional != null) { + return getDao().deleteByUrl(theConditional, theRequestDetails); + } else { + return getDao().delete(theResource, theRequestDetails); + } + } finally { + endRequest(theRequest); + } + } + + @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { + @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, typeName = "integer") + }) + public IBaseParameters expunge( + @IdParam IIdType theIdParam, + @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT, typeName = "integer") IPrimitiveType theLimit, + @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES, typeName = "boolean") IPrimitiveType theExpungeDeletedResources, + @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS, typeName = "boolean") IPrimitiveType theExpungeOldVersions, + RequestDetails theRequest) { + return doExpunge(theIdParam, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest); + } + + @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { + @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, typeName = "integer") + }) + public IBaseParameters expunge( + @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT, typeName = "integer") IPrimitiveType theLimit, + @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES, typeName = "boolean") IPrimitiveType theExpungeDeletedResources, + @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS, typeName = "boolean") IPrimitiveType theExpungeOldVersions, + RequestDetails theRequest) { + return doExpunge(null, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest); + } + + @Description("Request a global list of tags, profiles, and security labels") + @Operation(name = OPERATION_META, idempotent = true, returnParameters = { + @OperationParam(name = "return", typeName = "Meta") + }) + public IBaseParameters meta(RequestDetails theRequestDetails) { + Class metaType = getContext().getElementDefinition("Meta").getImplementingClass(); + IBaseMetaType metaGetOperation = getDao().metaGetOperation(metaType, theRequestDetails); + IBaseParameters parameters = ParametersUtil.newInstance(getContext()); + ParametersUtil.addParameterToParameters(getContext(), parameters, "return", metaGetOperation); + return parameters; + } + + @Description("Request a list of tags, profiles, and security labels for a specfic resource instance") + @Operation(name = OPERATION_META, idempotent = true, returnParameters = { + @OperationParam(name = "return", typeName = "Meta") + }) + public IBaseParameters meta(@IdParam IIdType theId, RequestDetails theRequestDetails) { + Class metaType = getContext().getElementDefinition("Meta").getImplementingClass(); + IBaseMetaType metaGetOperation = getDao().metaGetOperation(metaType, theId, theRequestDetails); + + IBaseParameters parameters = ParametersUtil.newInstance(getContext()); + ParametersUtil.addParameterToParameters(getContext(), parameters, "return", metaGetOperation); + return parameters; + } + + @Description("Add tags, profiles, and/or security labels to a resource") + @Operation(name = OPERATION_META_ADD, idempotent = false, returnParameters = { + @OperationParam(name = "return", typeName = "Meta") + }) + public IBaseParameters metaAdd(@IdParam IIdType theId, @OperationParam(name = "meta", typeName = "Meta") IBaseMetaType theMeta, RequestDetails theRequestDetails) { + if (theMeta == null) { + throw new InvalidRequestException("Input contains no parameter with name 'meta'"); + } + IBaseMetaType metaAddOperation = getDao().metaAddOperation(theId, theMeta, theRequestDetails); + IBaseParameters parameters = ParametersUtil.newInstance(getContext()); + ParametersUtil.addParameterToParameters(getContext(), parameters, "return", metaAddOperation); + return parameters; + } + + @Description("Delete tags, profiles, and/or security labels from a resource") + @Operation(name = OPERATION_META_DELETE, idempotent = false, returnParameters = { + @OperationParam(name = "return", typeName = "Meta") + }) + public IBaseParameters metaDelete(@IdParam IIdType theId, @OperationParam(name = "meta", typeName = "Meta") IBaseMetaType theMeta, RequestDetails theRequestDetails) { + if (theMeta == null) { + throw new InvalidRequestException("Input contains no parameter with name 'meta'"); + } + IBaseMetaType metaDelete = getDao().metaDeleteOperation(theId, theMeta, theRequestDetails); + IBaseParameters parameters = ParametersUtil.newInstance(getContext()); + ParametersUtil.addParameterToParameters(getContext(), parameters, "return", metaDelete); + return parameters; + } + + @Update + public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IIdType theId, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) { + startRequest(theRequest); + try { + if (theConditional != null) { + return getDao().update(theResource, theConditional, theRequestDetails); + } else { + return getDao().update(theResource, theRequestDetails); + } + } finally { + endRequest(theRequest); + } + } + + @Validate + public MethodOutcome validate(@ResourceParam T theResource, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, + @Validate.Profile String theProfile, RequestDetails theRequestDetails) { + return validate(theResource, null, theRawResource, theEncoding, theMode, theProfile, theRequestDetails); + } + + @Validate + public MethodOutcome validate(@ResourceParam T theResource, @IdParam IIdType theId, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, + @Validate.Profile String theProfile, RequestDetails theRequestDetails) { + return getDao().validate(theResource, theId, theRawResource, theEncoding, theMode, theProfile, theRequestDetails); + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProvider.java index 4bd29532d1f..84751fb6c96 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProvider.java @@ -23,15 +23,18 @@ package ca.uhn.fhir.jpa.provider; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.api.model.ExpungeOptions; import ca.uhn.fhir.jpa.api.model.ExpungeOutcome; +import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.search.reindex.IResourceReindexingSvc; import ca.uhn.fhir.rest.annotation.At; import ca.uhn.fhir.rest.annotation.History; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.Since; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.DateRangeParam; +import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r4.model.Parameters; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Required; @@ -55,7 +58,22 @@ public class BaseJpaSystemProvider extends BaseJpaProvider implements IJp return myResourceReindexingSvc; } - protected Parameters doExpunge(IPrimitiveType theLimit, IPrimitiveType theExpungeDeletedResources, IPrimitiveType theExpungeOldVersions, IPrimitiveType theExpungeEverything, RequestDetails theRequestDetails) { + @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { + @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, typeName = "integer") + }) + public IBaseParameters expunge( + @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT, typeName = "integer") IPrimitiveType theLimit, + @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES, typeName = "boolean") IPrimitiveType theExpungeDeletedResources, + @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS, typeName = "boolean") IPrimitiveType theExpungeOldVersions, + @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING, typeName = "boolean") IPrimitiveType theExpungeEverything, + RequestDetails theRequestDetails + ) { + ExpungeOptions options = createExpungeOptions(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything); + ExpungeOutcome outcome = getDao().expunge(options, theRequestDetails); + return createExpungeResponse(outcome); + } + + protected IBaseParameters doExpunge(IPrimitiveType theLimit, IPrimitiveType theExpungeDeletedResources, IPrimitiveType theExpungeOldVersions, IPrimitiveType theExpungeEverything, RequestDetails theRequestDetails) { ExpungeOptions options = createExpungeOptions(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything); ExpungeOutcome outcome = getDao().expunge(options, theRequestDetails); return createExpungeResponse(outcome); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProviderDstu2Plus.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProviderDstu2Plus.java index a1a3f68f303..9c3b95007d8 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProviderDstu2Plus.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/BaseJpaSystemProviderDstu2Plus.java @@ -20,19 +20,27 @@ package ca.uhn.fhir.jpa.provider; * #L% */ +import ca.uhn.fhir.jpa.model.util.JpaConstants; +import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.util.ParametersUtil; +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IPrimitiveType; +import javax.servlet.http.HttpServletRequest; + import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseJpaSystemProviderDstu2Plus extends BaseJpaSystemProvider { - @Operation(name = MARK_ALL_RESOURCES_FOR_REINDEXING, idempotent = true, returnParameters = { + @Description("Marks all currently existing resources of a given type, or all resources of all types, for reindexing.") + @Operation(name = MARK_ALL_RESOURCES_FOR_REINDEXING, idempotent = false, returnParameters = { @OperationParam(name = "status") }) public IBaseResource markAllResourcesForReindexing( @@ -53,7 +61,8 @@ public abstract class BaseJpaSystemProviderDstu2Plus extends BaseJpaSyste return retVal; } - @Operation(name = PERFORM_REINDEXING_PASS, idempotent = true, returnParameters = { + @Description("Forces a single pass of the resource reindexing processor") + @Operation(name = PERFORM_REINDEXING_PASS, idempotent = false, returnParameters = { @OperationParam(name = "status") }) public IBaseResource performReindexingPass() { @@ -72,4 +81,28 @@ public abstract class BaseJpaSystemProviderDstu2Plus extends BaseJpaSyste return retVal; } + /** + * $process-message + */ + @Description("Accept a FHIR Message Bundle for processing") + @Operation(name = JpaConstants.OPERATION_PROCESS_MESSAGE, idempotent = false) + public IBaseBundle processMessage( + HttpServletRequest theServletRequest, + RequestDetails theRequestDetails, + + @OperationParam(name = "content", min = 1, max = 1, typeName = "Bundle") + @Description(formalDefinition = "The message to process (or, if using asynchronous messaging, it may be a response message to accept)") + IBaseBundle theMessageToProcess + ) { + + startRequest(theServletRequest); + try { + return getDao().processMessage(theRequestDetails, theMessageToProcess); + } finally { + endRequest(theServletRequest); + } + + } + + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/DiffProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/DiffProvider.java index cc2f98a7797..2f82366aca9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/DiffProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/DiffProvider.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.patch.FhirPatch; +import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; @@ -49,14 +50,23 @@ public class DiffProvider { @Autowired private DaoRegistry myDaoRegistry; + @Description( + value="This operation examines two resource versions (can be two versions of the same resource, or two different resources) and generates a FHIR Patch document showing the differences.", + shortDefinition = "Comparte two resources or two versions of a single resource") @Operation(name = ProviderConstants.DIFF_OPERATION_NAME, global = true, idempotent = true) public IBaseParameters diff( @IdParam IIdType theResourceId, - @OperationParam(name = ProviderConstants.DIFF_FROM_VERSION_PARAMETER, typeName = "string", min = 0, max = 1) IPrimitiveType theFromVersion, - @OperationParam(name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER, typeName = "boolean", min = 0, max = 1) IPrimitiveType theIncludeMeta, + + @Description(value = "The resource ID and version to diff from", example = "Patient/example/version/1") + @OperationParam(name = ProviderConstants.DIFF_FROM_VERSION_PARAMETER, typeName = "string", min = 0, max = 1) + IPrimitiveType theFromVersion, + + @Description(value = "Should differences in the Resource.meta element be included in the diff", example = "false") + @OperationParam(name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER, typeName = "boolean", min = 0, max = 1) + IPrimitiveType theIncludeMeta, RequestDetails theRequestDetails) { - IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResourceId.getResourceType()); + IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResourceId.getResourceType()); IBaseResource targetResource = dao.read(theResourceId, theRequestDetails); IBaseResource sourceResource = null; @@ -82,15 +92,23 @@ public class DiffProvider { } FhirPatch fhirPatch = newPatch(theIncludeMeta); - IBaseParameters diff = fhirPatch.diff(sourceResource, targetResource); - return diff; + return fhirPatch.diff(sourceResource, targetResource); } + @Description("This operation examines two resource versions (can be two versions of the same resource, or two different resources) and generates a FHIR Patch document showing the differences.") @Operation(name = ProviderConstants.DIFF_OPERATION_NAME, idempotent = true) public IBaseParameters diff( - @OperationParam(name = ProviderConstants.DIFF_FROM_PARAMETER, typeName = "id", min = 1, max = 1) IIdType theFromVersion, - @OperationParam(name = ProviderConstants.DIFF_TO_PARAMETER, typeName = "id", min = 1, max = 1) IIdType theToVersion, - @OperationParam(name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER, typeName = "boolean", min = 0, max = 1) IPrimitiveType theIncludeMeta, + @Description(value = "The resource ID and version to diff from", example = "Patient/example/version/1") + @OperationParam(name = ProviderConstants.DIFF_FROM_PARAMETER, typeName = "id", min = 1, max = 1) + IIdType theFromVersion, + + @Description(value = "The resource ID and version to diff to", example = "Patient/example/version/2") + @OperationParam(name = ProviderConstants.DIFF_TO_PARAMETER, typeName = "id", min = 1, max = 1) + IIdType theToVersion, + + @Description(value = "Should differences in the Resource.meta element be included in the diff", example = "false") + @OperationParam(name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER, typeName = "boolean", min = 0, max = 1) + IPrimitiveType theIncludeMeta, RequestDetails theRequestDetails) { if (!Objects.equal(theFromVersion.getResourceType(), theToVersion.getResourceType())) { @@ -98,13 +116,12 @@ public class DiffProvider { throw new InvalidRequestException(msg); } - IFhirResourceDao dao = myDaoRegistry.getResourceDao(theFromVersion.getResourceType()); + IFhirResourceDao dao = myDaoRegistry.getResourceDao(theFromVersion.getResourceType()); IBaseResource sourceResource = dao.read(theFromVersion, theRequestDetails); IBaseResource targetResource = dao.read(theToVersion, theRequestDetails); FhirPatch fhirPatch = newPatch(theIncludeMeta); - IBaseParameters diff = fhirPatch.diff(sourceResource, targetResource); - return diff; + return fhirPatch.diff(sourceResource, targetResource); } @Nonnull diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java index f3d27f59a3e..ed68657798c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/GraphQLProvider.java @@ -24,6 +24,7 @@ import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.context.support.IValidationSupport; +import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.rest.annotation.GraphQL; import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; @@ -110,18 +111,20 @@ public class GraphQLProvider { myStorageServices = theStorageServices; } + @Description(value="This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.") @GraphQL(type=RequestTypeEnum.GET) - public String processGraphQlGetRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String queryUrl) { - if (queryUrl != null) { - return processGraphQLRequest(theRequestDetails, theId, queryUrl); + public String processGraphQlGetRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String theQueryUrl) { + if (theQueryUrl != null) { + return processGraphQLRequest(theRequestDetails, theId, theQueryUrl); } throw new InvalidRequestException("Unable to parse empty GraphQL expression"); } + @Description(value="This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.") @GraphQL(type=RequestTypeEnum.POST) - public String processGraphQlPostRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryBody String queryBody) { - if (queryBody != null) { - return processGraphQLRequest(theRequestDetails, theId, queryBody); + public String processGraphQlPostRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryBody String theQueryBody) { + if (theQueryBody != null) { + return processGraphQLRequest(theRequestDetails, theId, theQueryBody); } throw new InvalidRequestException("Unable to parse empty GraphQL expression"); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaCapabilityStatementProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaCapabilityStatementProvider.java index 07c0fd5769c..08c123988cd 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaCapabilityStatementProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaCapabilityStatementProvider.java @@ -80,6 +80,11 @@ public class JpaCapabilityStatementProvider extends ServerCapabilityStatementPro if (isNotBlank(myImplementationDescription)) { theTerser.setElement(theCapabilityStatement, "implementation.description", myImplementationDescription); } + + theTerser.addElement(theCapabilityStatement, "patchFormat", Constants.CT_FHIR_JSON_NEW); + theTerser.addElement(theCapabilityStatement, "patchFormat", Constants.CT_FHIR_XML_NEW); + theTerser.addElement(theCapabilityStatement, "patchFormat", Constants.CT_JSON_PATCH); + theTerser.addElement(theCapabilityStatement, "patchFormat", Constants.CT_XML_PATCH); } @Override diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaResourceProviderDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaResourceProviderDstu2.java index 92b926c8f10..53f0ca41d3e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaResourceProviderDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaResourceProviderDstu2.java @@ -63,129 +63,4 @@ public class JpaResourceProviderDstu2 extends BaseJpaResour super(theDao); } - @Create - public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) { - startRequest(theRequest); - try { - if (theConditional != null) { - return getDao().create(theResource, theConditional, theRequestDetails); - } else { - return getDao().create(theResource, theRequestDetails); - } - } finally { - endRequest(theRequest); - } - } - - @Delete() - public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IdDt theResource, @ConditionalUrlParam(supportsMultiple = true) String theConditional, RequestDetails theRequestDetails) { - startRequest(theRequest); - try { - if (theConditional != null) { - return getDao().deleteByUrl(theConditional, theRequestDetails); - } else { - return getDao().delete(theResource, theRequestDetails); - } - } finally { - endRequest(theRequest); - } - } - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerDt.class) - }) - public Parameters expunge( - @IdParam IIdType theIdParam, - @OperationParam(name = OPERATION_EXPUNGE_PARAM_LIMIT) IntegerDt theLimit, - @OperationParam(name = OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanDt theExpungeDeletedResources, - @OperationParam(name = OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanDt theExpungeOldVersions, - RequestDetails theRequest) { - org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(theIdParam, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest); - return JpaSystemProviderDstu2.toExpungeResponse(retVal); - } - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerDt.class) - }) - public Parameters expunge( - @OperationParam(name = OPERATION_EXPUNGE_PARAM_LIMIT) IntegerDt theLimit, - @OperationParam(name = OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanDt theExpungeDeletedResources, - @OperationParam(name = OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanDt theExpungeOldVersions, - RequestDetails theRequest) { - org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(null, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest); - return JpaSystemProviderDstu2.toExpungeResponse(retVal); - } - - @Operation(name = OPERATION_META, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = MetaDt.class) - }) - public Parameters meta(RequestDetails theRequestDetails) { - Parameters parameters = new Parameters(); - MetaDt metaGetOperation = getDao().metaGetOperation(MetaDt.class, theRequestDetails); - parameters.addParameter().setName("return").setValue(metaGetOperation); - return parameters; - } - - @Operation(name = OPERATION_META, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = MetaDt.class) - }) - public Parameters meta(@IdParam IdDt theId, RequestDetails theRequestDetails) { - Parameters parameters = new Parameters(); - MetaDt metaGetOperation = getDao().metaGetOperation(MetaDt.class, theId, theRequestDetails); - parameters.addParameter().setName("return").setValue(metaGetOperation); - return parameters; - } - - @Operation(name = OPERATION_META_ADD, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = MetaDt.class) - }) - public Parameters metaAdd(@IdParam IdDt theId, @OperationParam(name = "meta") MetaDt theMeta, RequestDetails theRequestDetails) { - if (theMeta == null) { - throw new InvalidRequestException("Input contains no parameter with name 'meta'"); - } - Parameters parameters = new Parameters(); - MetaDt metaAddOperation = getDao().metaAddOperation(theId, theMeta, theRequestDetails); - parameters.addParameter().setName("return").setValue(metaAddOperation); - return parameters; - } - - @Operation(name = OPERATION_META_DELETE, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = MetaDt.class) - }) - public Parameters metaDelete(@IdParam IdDt theId, @OperationParam(name = "meta") MetaDt theMeta, RequestDetails theRequestDetails) { - if (theMeta == null) { - throw new InvalidRequestException("Input contains no parameter with name 'meta'"); - } - Parameters parameters = new Parameters(); - parameters.addParameter().setName("return").setValue(getDao().metaDeleteOperation(theId, theMeta, theRequestDetails)); - return parameters; - } - - @Update - public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IdDt theId, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) { - startRequest(theRequest); - try { - if (theConditional != null) { - return getDao().update(theResource, theConditional, theRequestDetails); - } else { - theResource.setId(theId); - return getDao().update(theResource, theRequestDetails); - } - } finally { - endRequest(theRequest); - } - } - - @Validate - public MethodOutcome validate(@ResourceParam T theResource, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, - @Validate.Profile String theProfile, RequestDetails theRequestDetails) { - return validate(theResource, null, theRawResource, theEncoding, theMode, theProfile, theRequestDetails); - } - - @Validate - public MethodOutcome validate(@ResourceParam T theResource, @IdParam IdDt theId, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, - @Validate.Profile String theProfile, RequestDetails theRequestDetails) { - return getDao().validate(theResource, theId, theRawResource, theEncoding, theMode, theProfile, theRequestDetails); - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java index 538e7a9a283..b7c5f32596b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaSystemProviderDstu2.java @@ -1,38 +1,30 @@ package ca.uhn.fhir.jpa.provider; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; -import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.dstu2.composite.MetaDt; import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.model.dstu2.resource.Parameters; import ca.uhn.fhir.model.dstu2.resource.Parameters.Parameter; -import ca.uhn.fhir.model.primitive.BooleanDt; import ca.uhn.fhir.model.primitive.IntegerDt; import ca.uhn.fhir.model.primitive.StringDt; -import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.Transaction; import ca.uhn.fhir.rest.annotation.TransactionParam; import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import org.hl7.fhir.instance.model.api.IBaseBundle; -import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.IntegerType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import javax.servlet.http.HttpServletRequest; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; -import static org.apache.commons.lang3.StringUtils.isBlank; /* * #%L @@ -60,38 +52,6 @@ public class JpaSystemProviderDstu2 extends BaseJpaSystemProviderDstu2Plus mySystemDao; - @Autowired(required = false) - private IFulltextSearchSvc mySearchDao; - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerDt.class) - }) - public Parameters expunge( - @IdParam IIdType theIdParam, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerDt theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanDt theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanDt theExpungeOldVersions, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanDt theExpungeEverything, - RequestDetails theRequestDetails - ) { - org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails); - return toExpungeResponse(retVal); - } - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerDt.class) - }) - public Parameters expunge( - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerDt theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanDt theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanDt theExpungeOldVersions, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanDt theExpungeEverything, - RequestDetails theRequestDetails - ) { - org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails); - return toExpungeResponse(retVal); - } - //@formatter:off // This is generated by hand: // ls hapi-fhir-structures-dstu2/target/generated-sources/tinder/ca/uhn/fhir/model/dstu2/resource/ | sort | sed "s/.java//" | sed "s/^/@OperationParam(name=\"/" | sed "s/$/\", type=IntegerDt.class, min=0, max=1),/" @@ -222,28 +182,6 @@ public class JpaSystemProviderDstu2 extends BaseJpaSystemProviderDstu2Plus extends BaseJpaRes super(theDao); } - @Create - public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) { - startRequest(theRequest); - try { - if (theConditional != null) { - return getDao().create(theResource, theConditional, theRequestDetails); - } else { - return getDao().create(theResource, theRequestDetails); - } - } finally { - endRequest(theRequest); - } - } - - @Delete() - public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IdType theResource, @ConditionalUrlParam(supportsMultiple = true) String theConditional, RequestDetails theRequestDetails) { - startRequest(theRequest); - try { - if (theConditional != null) { - return getDao().deleteByUrl(theConditional, theRequestDetails); - } else { - return getDao().delete(theResource, theRequestDetails); - } - } finally { - endRequest(theRequest); - } - } - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class) - }) - public Parameters expunge( - @IdParam IIdType theIdParam, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions, - RequestDetails theRequest) { - org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(theIdParam, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest); - try { - return convertParameters(retVal); - } catch (FHIRException e) { - throw new InternalErrorException(e); - } - } - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class) - }) - public Parameters expunge( - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions, - RequestDetails theRequest) { - org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(null, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest); - try { - return convertParameters(retVal); - } catch (FHIRException e) { - throw new InternalErrorException(e); - } - } - - @Operation(name = OPERATION_META, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = Meta.class) - }) - public Parameters meta(RequestDetails theRequestDetails) { - Parameters parameters = new Parameters(); - Meta metaGetOperation = getDao().metaGetOperation(Meta.class, theRequestDetails); - parameters.addParameter().setName("return").setValue(metaGetOperation); - return parameters; - } - - @Operation(name = OPERATION_META, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = Meta.class) - }) - public Parameters meta(@IdParam IdType theId, RequestDetails theRequestDetails) { - Parameters parameters = new Parameters(); - Meta metaGetOperation = getDao().metaGetOperation(Meta.class, theId, theRequestDetails); - parameters.addParameter().setName("return").setValue(metaGetOperation); - return parameters; - } - - @Operation(name = OPERATION_META_ADD, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = Meta.class) - }) - public Parameters metaAdd(@IdParam IdType theId, @OperationParam(name = "meta") Meta theMeta, RequestDetails theRequestDetails) { - if (theMeta == null) { - throw new InvalidRequestException("Input contains no parameter with name 'meta'"); - } - Parameters parameters = new Parameters(); - Meta metaAddOperation = getDao().metaAddOperation(theId, theMeta, theRequestDetails); - parameters.addParameter().setName("return").setValue(metaAddOperation); - return parameters; - } - - @Operation(name = OPERATION_META_DELETE, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = Meta.class) - }) - public Parameters metaDelete(@IdParam IdType theId, @OperationParam(name = "meta") Meta theMeta, RequestDetails theRequestDetails) { - if (theMeta == null) { - throw new InvalidRequestException("Input contains no parameter with name 'meta'"); - } - Parameters parameters = new Parameters(); - parameters.addParameter().setName("return").setValue(getDao().metaDeleteOperation(theId, theMeta, theRequestDetails)); - return parameters; - } - - @Update - public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IdType theId, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) { - startRequest(theRequest); - try { - if (theConditional != null) { - return getDao().update(theResource, theConditional, theRequestDetails); - } else { - return getDao().update(theResource, theRequestDetails); - } - } finally { - endRequest(theRequest); - } - } - - @Validate - public MethodOutcome validate(@ResourceParam T theResource, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, - @Validate.Profile String theProfile, RequestDetails theRequestDetails) { - return validate(theResource, null, theRawResource, theEncoding, theMode, theProfile, theRequestDetails); - } - - @Validate - public MethodOutcome validate(@ResourceParam T theResource, @IdParam IdType theId, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, - @Validate.Profile String theProfile, RequestDetails theRequestDetails) { - return getDao().validate(theResource, theId, theRawResource, theEncoding, theMode, theProfile, theRequestDetails); - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaSystemProviderDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaSystemProviderDstu3.java index 7cd6746cd48..18f89c03889 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaSystemProviderDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/dstu3/JpaSystemProviderDstu3.java @@ -65,46 +65,6 @@ public class JpaSystemProviderDstu3 extends BaseJpaSystemProviderDstu2Plus mySystemDao; - @Autowired(required = false) - private IFulltextSearchSvc mySearchDao; - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class) - }) - public Parameters expunge( - @IdParam IIdType theIdParam, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanType theExpungeEverything, - RequestDetails theRequestDetails - ) { - org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails); - try { - return convertParameters(retVal); - } catch (FHIRException e) { - throw new InternalErrorException(e); - } - } - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class) - }) - public Parameters expunge( - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanType theExpungeEverything, - RequestDetails theRequestDetails - ) { - org.hl7.fhir.r4.model.Parameters retVal = super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails); - try { - return convertParameters(retVal); - } catch (FHIRException e) { - throw new InternalErrorException(e); - } - } - // This is generated by hand: // ls hapi-fhir-structures-dstu2/target/generated-sources/tinder/ca/uhn/fhir/model/dstu2/resource/ | sort | sed "s/.java//" | sed "s/^/@OperationParam(name=\"/" | sed "s/$/\", type=IntegerType.class, min=0, max=1),/" @Operation(name = JpaConstants.OPERATION_GET_RESOURCE_COUNTS, idempotent = true, returnParameters = { @@ -233,32 +193,4 @@ public class JpaSystemProviderDstu3 extends BaseJpaSystemProviderDstu2Plus extends BaseJpaResour super(theDao); } - @Create - public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) { - startRequest(theRequest); - try { - if (theConditional != null) { - return getDao().create(theResource, theConditional, theRequestDetails); - } else { - return getDao().create(theResource, theRequestDetails); - } - } finally { - endRequest(theRequest); - } - } - - @Delete() - public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IdType theResource, @ConditionalUrlParam(supportsMultiple = true) String theConditional, RequestDetails theRequestDetails) { - startRequest(theRequest); - try { - if (theConditional != null) { - return getDao().deleteByUrl(theConditional, theRequestDetails); - } else { - return getDao().delete(theResource, theRequestDetails); - } - } finally { - endRequest(theRequest); - } - } - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class) - }) - public Parameters expunge( - @IdParam IIdType theIdParam, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions, - RequestDetails theRequest) { - return super.doExpunge(theIdParam, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest); - } - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class) - }) - public Parameters expunge( - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions, - RequestDetails theRequest) { - return super.doExpunge(null, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest); - } - - - - @Operation(name = OPERATION_META, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = Meta.class) - }) - public Parameters meta(RequestDetails theRequestDetails) { - Parameters parameters = new Parameters(); - Meta metaGetOperation = getDao().metaGetOperation(Meta.class, theRequestDetails); - parameters.addParameter().setName("return").setValue(metaGetOperation); - return parameters; - } - - @Operation(name = OPERATION_META, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = Meta.class) - }) - public Parameters meta(@IdParam IdType theId, RequestDetails theRequestDetails) { - Parameters parameters = new Parameters(); - Meta metaGetOperation = getDao().metaGetOperation(Meta.class, theId, theRequestDetails); - parameters.addParameter().setName("return").setValue(metaGetOperation); - return parameters; - } - - @Operation(name = OPERATION_META_ADD, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = Meta.class) - }) - public Parameters metaAdd(@IdParam IdType theId, @OperationParam(name = "meta") Meta theMeta, RequestDetails theRequestDetails) { - if (theMeta == null) { - throw new InvalidRequestException("Input contains no parameter with name 'meta'"); - } - Parameters parameters = new Parameters(); - Meta metaAddOperation = getDao().metaAddOperation(theId, theMeta, theRequestDetails); - parameters.addParameter().setName("return").setValue(metaAddOperation); - return parameters; - } - - @Operation(name = OPERATION_META_DELETE, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = Meta.class) - }) - public Parameters metaDelete(@IdParam IdType theId, @OperationParam(name = "meta") Meta theMeta, RequestDetails theRequestDetails) { - if (theMeta == null) { - throw new InvalidRequestException("Input contains no parameter with name 'meta'"); - } - Parameters parameters = new Parameters(); - parameters.addParameter().setName("return").setValue(getDao().metaDeleteOperation(theId, theMeta, theRequestDetails)); - return parameters; - } - - @Update - public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IdType theId, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) { - startRequest(theRequest); - try { - if (theConditional != null) { - return getDao().update(theResource, theConditional, theRequestDetails); - } else { - return getDao().update(theResource, theRequestDetails); - } - } finally { - endRequest(theRequest); - } - } - - @Validate - public MethodOutcome validate(@ResourceParam T theResource, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, - @Validate.Profile String theProfile, RequestDetails theRequestDetails) { - return validate(theResource, null, theRawResource, theEncoding, theMode, theProfile, theRequestDetails); - } - - @Validate - public MethodOutcome validate(@ResourceParam T theResource, @IdParam IdType theId, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, - @Validate.Profile String theProfile, RequestDetails theRequestDetails) { - return getDao().validate(theResource, theId, theRawResource, theEncoding, theMode, theProfile, theRequestDetails); - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaSystemProviderR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaSystemProviderR4.java index 662efc47afd..b77a4a05c54 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaSystemProviderR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r4/JpaSystemProviderR4.java @@ -1,40 +1,28 @@ package ca.uhn.fhir.jpa.provider.r4; import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao; -import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.provider.BaseJpaSystemProviderDstu2Plus; import ca.uhn.fhir.model.api.annotation.Description; -import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.Transaction; import ca.uhn.fhir.rest.annotation.TransactionParam; import ca.uhn.fhir.rest.api.server.RequestDetails; -import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import org.hl7.fhir.instance.model.api.IBaseBundle; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.DecimalType; import org.hl7.fhir.r4.model.IntegerType; import org.hl7.fhir.r4.model.Meta; import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent; -import org.hl7.fhir.r4.model.StringType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import javax.servlet.http.HttpServletRequest; import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; -import static org.apache.commons.lang3.StringUtils.isBlank; /* * #%L @@ -62,36 +50,6 @@ public class JpaSystemProviderR4 extends BaseJpaSystemProviderDstu2Plus mySystemDao; - @Autowired(required = false) - private IFulltextSearchSvc mySearchDao; - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class) - }) - public Parameters expunge( - @IdParam IIdType theIdParam, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanType theExpungeEverything, - RequestDetails theRequestDetails - ) { - return super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails); - } - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class) - }) - public Parameters expunge( - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanType theExpungeEverything, - RequestDetails theRequestDetails - ) { - return super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails); - } - // This is generated by hand: // ls hapi-fhir-structures-dstu2/target/generated-sources/tinder/ca/uhn/fhir/model/dstu2/resource/ | sort | sed "s/.java//" | sed "s/^/@OperationParam(name=\"/" | sed "s/$/\", type=IntegerType.class, min=0, max=1),/" @Operation(name = JpaConstants.OPERATION_GET_RESOURCE_COUNTS, idempotent = true, returnParameters = { @@ -210,28 +168,6 @@ public class JpaSystemProviderR4 extends BaseJpaSystemProviderDstu2Plus extends BaseJpaResour super(theDao); } - @Create - public MethodOutcome create(HttpServletRequest theRequest, @ResourceParam T theResource, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) { - startRequest(theRequest); - try { - if (theConditional != null) { - return getDao().create(theResource, theConditional, theRequestDetails); - } else { - return getDao().create(theResource, theRequestDetails); - } - } finally { - endRequest(theRequest); - } - } - - @Delete() - public MethodOutcome delete(HttpServletRequest theRequest, @IdParam IdType theResource, @ConditionalUrlParam(supportsMultiple = true) String theConditional, RequestDetails theRequestDetails) { - startRequest(theRequest); - try { - if (theConditional != null) { - return getDao().deleteByUrl(theConditional, theRequestDetails); - } else { - return getDao().delete(theResource, theRequestDetails); - } - } finally { - endRequest(theRequest); - } - } - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class) - }) - public Parameters expunge( - @IdParam IIdType theIdParam, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions, - RequestDetails theRequest) { - - org.hl7.fhir.r4.model.Parameters parameters = super.doExpunge(theIdParam, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest); - return org.hl7.fhir.convertors.conv40_50.Parameters40_50.convertParameters(parameters); - - } - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class) - }) - public Parameters expunge( - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions, - RequestDetails theRequest) { - org.hl7.fhir.r4.model.Parameters parameters = super.doExpunge(null, theLimit, theExpungeDeletedResources, theExpungeOldVersions, null, theRequest); - return org.hl7.fhir.convertors.conv40_50.Parameters40_50.convertParameters(parameters); - } - - @Operation(name = OPERATION_META, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = Meta.class) - }) - public Parameters meta(RequestDetails theRequestDetails) { - Parameters parameters = new Parameters(); - Meta metaGetOperation = getDao().metaGetOperation(Meta.class, theRequestDetails); - parameters.addParameter().setName("return").setValue(metaGetOperation); - return parameters; - } - - @Operation(name = OPERATION_META, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = Meta.class) - }) - public Parameters meta(@IdParam IdType theId, RequestDetails theRequestDetails) { - Parameters parameters = new Parameters(); - Meta metaGetOperation = getDao().metaGetOperation(Meta.class, theId, theRequestDetails); - parameters.addParameter().setName("return").setValue(metaGetOperation); - return parameters; - } - - @Operation(name = OPERATION_META_ADD, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = Meta.class) - }) - public Parameters metaAdd(@IdParam IdType theId, @OperationParam(name = "meta") Meta theMeta, RequestDetails theRequestDetails) { - if (theMeta == null) { - throw new InvalidRequestException("Input contains no parameter with name 'meta'"); - } - Parameters parameters = new Parameters(); - Meta metaAddOperation = getDao().metaAddOperation(theId, theMeta, theRequestDetails); - parameters.addParameter().setName("return").setValue(metaAddOperation); - return parameters; - } - - @Operation(name = OPERATION_META_DELETE, idempotent = true, returnParameters = { - @OperationParam(name = "return", type = Meta.class) - }) - public Parameters metaDelete(@IdParam IdType theId, @OperationParam(name = "meta") Meta theMeta, RequestDetails theRequestDetails) { - if (theMeta == null) { - throw new InvalidRequestException("Input contains no parameter with name 'meta'"); - } - Parameters parameters = new Parameters(); - parameters.addParameter().setName("return").setValue(getDao().metaDeleteOperation(theId, theMeta, theRequestDetails)); - return parameters; - } - - @Update - public MethodOutcome update(HttpServletRequest theRequest, @ResourceParam T theResource, @IdParam IdType theId, @ConditionalUrlParam String theConditional, RequestDetails theRequestDetails) { - startRequest(theRequest); - try { - if (theConditional != null) { - return getDao().update(theResource, theConditional, theRequestDetails); - } else { - return getDao().update(theResource, theRequestDetails); - } - } finally { - endRequest(theRequest); - } - } - - @Validate - public MethodOutcome validate(@ResourceParam T theResource, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, - @Validate.Profile String theProfile, RequestDetails theRequestDetails) { - return validate(theResource, null, theRawResource, theEncoding, theMode, theProfile, theRequestDetails); - } - - @Validate - public MethodOutcome validate(@ResourceParam T theResource, @IdParam IdType theId, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, - @Validate.Profile String theProfile, RequestDetails theRequestDetails) { - return getDao().validate(theResource, theId, theRawResource, theEncoding, theMode, theProfile, theRequestDetails); - } - } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaSystemProviderR5.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaSystemProviderR5.java index 6cb29aa50f2..631645099b4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaSystemProviderR5.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/r5/JpaSystemProviderR5.java @@ -62,38 +62,6 @@ public class JpaSystemProviderR5 extends BaseJpaSystemProviderDstu2Plus mySystemDao; - @Autowired(required = false) - private IFulltextSearchSvc mySearchDao; - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class) - }) - public Parameters expunge( - @IdParam IIdType theIdParam, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanType theExpungeEverything, - RequestDetails theRequestDetails - ) { - org.hl7.fhir.r4.model.Parameters parameters = super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails); - return org.hl7.fhir.convertors.conv40_50.Parameters40_50.convertParameters(parameters); - } - - @Operation(name = JpaConstants.OPERATION_EXPUNGE, idempotent = false, returnParameters = { - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_OUT_PARAM_EXPUNGE_COUNT, type = IntegerType.class) - }) - public Parameters expunge( - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_LIMIT) IntegerType theLimit, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_DELETED_RESOURCES) BooleanType theExpungeDeletedResources, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_PREVIOUS_VERSIONS) BooleanType theExpungeOldVersions, - @OperationParam(name = JpaConstants.OPERATION_EXPUNGE_PARAM_EXPUNGE_EVERYTHING) BooleanType theExpungeEverything, - RequestDetails theRequestDetails - ) { - org.hl7.fhir.r4.model.Parameters parameters = super.doExpunge(theLimit, theExpungeDeletedResources, theExpungeOldVersions, theExpungeEverything, theRequestDetails); - return org.hl7.fhir.convertors.conv40_50.Parameters40_50.convertParameters(parameters); - } - // This is generated by hand: // ls hapi-fhir-structures-dstu2/target/generated-sources/tinder/ca/uhn/fhir/model/dstu2/resource/ | sort | sed "s/.java//" | sed "s/^/@OperationParam(name=\"/" | sed "s/$/\", type=IntegerType.class, min=0, max=1),/" @Operation(name = JpaConstants.OPERATION_GET_RESOURCE_COUNTS, idempotent = true, returnParameters = { @@ -212,28 +180,6 @@ public class JpaSystemProviderR5 extends BaseJpaSystemProviderDstu2Plus ids = toUnqualifiedVersionlessIds(results); + myCaptureQueriesListener.logSelectQueries(); + + assertEquals(10, ids.size(), () -> ids.toString()); + } + @Test public void testSearch_TagNotParam_SearchAllPartitions() { IIdType patientIdNull = createPatient(withPartition(null), withActiveTrue(), withTag("http://system", "code"), withIdentifier("http://foo", "bar")); @@ -2914,7 +2948,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test { myResourceReindexingSvc.markAllResourcesForReindexing(); myResourceReindexingSvc.forceReindexingPass(); - runInTransaction(()->{ + runInTransaction(() -> { assertNotEquals(BaseHapiFhirDao.INDEX_STATUS_INDEXING_FAILED, myResourceTableDao.findById(patientIdNull.getIdPartAsLong()).get().getIndexStatus()); assertNotEquals(BaseHapiFhirDao.INDEX_STATUS_INDEXING_FAILED, myResourceTableDao.findById(patientId1.getIdPartAsLong()).get().getIndexStatus()); }); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java index d1dbf9d1f30..bcaba0b3547 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/SystemProviderDstu2Test.java @@ -23,6 +23,7 @@ import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; @@ -387,8 +388,8 @@ public class SystemProviderDstu2Test extends BaseJpaDstu2Test { @Test public void testMarkResourcesForReindexing() throws Exception { - HttpGet get = new HttpGet(ourServerBase + "/$mark-all-resources-for-reindexing"); - CloseableHttpResponse http = ourHttpClient.execute(get); + HttpPost post = new HttpPost(ourServerBase + "/$mark-all-resources-for-reindexing"); + CloseableHttpResponse http = ourHttpClient.execute(post); try { String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(output); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ServerDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ServerDstu3Test.java index db6bde81d17..8a56f262375 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ServerDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ServerDstu3Test.java @@ -9,6 +9,7 @@ import java.nio.charset.StandardCharsets; import java.util.HashSet; import java.util.Set; +import ca.uhn.fhir.rest.openapi.OpenApiInterceptor; import org.apache.commons.io.IOUtils; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -16,6 +17,7 @@ import org.hl7.fhir.dstu3.model.CapabilityStatement; import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestResourceComponent; import org.hl7.fhir.dstu3.model.CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import ca.uhn.fhir.util.TestUtil; @@ -24,7 +26,13 @@ public class ServerDstu3Test extends BaseResourceProviderDstu3Test { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerDstu3Test.class); - + @Override + @AfterEach + public void after() throws Exception { + super.after(); + ourRestServer.getInterceptorService().unregisterInterceptorsIf(t->t instanceof OpenApiInterceptor); + } + /** * See #519 @@ -61,5 +69,18 @@ public class ServerDstu3Test extends BaseResourceProviderDstu3Test { } + @Test + public void testFetchOpenApi() throws IOException { + ourRestServer.registerInterceptor(new OpenApiInterceptor()); + + HttpGet get = new HttpGet(ourServerBase + "/api-docs"); + try (CloseableHttpResponse response = ourHttpClient.execute(get)) { + String string = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(string); + + assertEquals(200, response.getStatusLine().getStatusCode()); + } + } + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/OpenApiInterceptorJpaTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/OpenApiInterceptorJpaTest.java new file mode 100644 index 00000000000..5ca0f688fca --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/OpenApiInterceptorJpaTest.java @@ -0,0 +1,48 @@ +package ca.uhn.fhir.jpa.provider.r4; + +import ca.uhn.fhir.rest.openapi.OpenApiInterceptor; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class OpenApiInterceptorJpaTest extends BaseResourceProviderR4Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OpenApiInterceptorJpaTest.class); + + @Override + @AfterEach + public void after() throws Exception { + super.after(); + ourRestServer.getInterceptorService().unregisterInterceptorsIf(t -> t instanceof OpenApiInterceptor); + } + + @Test + public void testFetchOpenApi() throws IOException { + ourRestServer.registerInterceptor(new OpenApiInterceptor()); + + HttpGet get = new HttpGet(ourServerBase + "/metadata?_format=json&_pretty=true"); + try (CloseableHttpResponse response = ourHttpClient.execute(get)) { + String string = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(string); + + assertEquals(200, response.getStatusLine().getStatusCode()); + } + + get = new HttpGet(ourServerBase + "/api-docs"); + try (CloseableHttpResponse response = ourHttpClient.execute(get)) { + String string = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(string); + + assertEquals(200, response.getStatusLine().getStatusCode()); + } + } + + +} diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java index 9f4d6945e5c..eda0afa7e9c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerCapabilityStatementProviderJpaR4Test.java @@ -17,6 +17,7 @@ import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.Nonnull; import java.io.IOException; import java.util.List; +import java.util.TreeSet; import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; @@ -26,6 +27,7 @@ import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.not; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProviderR4Test { @@ -39,6 +41,38 @@ public class ServerCapabilityStatementProviderJpaR4Test extends BaseResourceProv assertThat(resourceTypes, hasItems("Patient", "Observation", "SearchParameter")); } + + @Test + public void testNoDuplicateResourceOperationNames() { + CapabilityStatement cs = myClient.capabilities().ofType(CapabilityStatement.class).execute(); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(cs)); + for (CapabilityStatement.CapabilityStatementRestResourceComponent next : cs.getRestFirstRep().getResource()) { + List opNames = next + .getOperation() + .stream() + .map(t -> t.getName()) + .sorted() + .collect(Collectors.toList()); + ourLog.info("System ops: {}", opNames); + assertEquals(opNames.stream().distinct().sorted().collect(Collectors.toList()), opNames); + } + } + + @Test + public void testNoDuplicateSystemOperationNames() { + CapabilityStatement cs = myClient.capabilities().ofType(CapabilityStatement.class).execute(); + ourLog.info(myFhirCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(cs)); + List systemOpNames = cs + .getRestFirstRep() + .getOperation() + .stream() + .map(t -> t.getName()) + .sorted() + .collect(Collectors.toList()); + ourLog.info("System ops: {}", systemOpNames); + assertEquals(systemOpNames.stream().distinct().sorted().collect(Collectors.toList()), systemOpNames); + } + @Test public void testCustomSearchParamsReflectedInSearchParams() { SearchParameter fooSp = new SearchParameter(); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java index 0b7976947cb..759f065ed1f 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/SystemProviderR4Test.java @@ -18,6 +18,7 @@ import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.client.apache.ResourceEntity; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor; import ca.uhn.fhir.rest.param.ReferenceParam; @@ -36,6 +37,7 @@ import org.apache.http.Header; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; @@ -49,6 +51,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleType; import org.hl7.fhir.r4.model.Bundle.HTTPVerb; +import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.DecimalType; import org.hl7.fhir.r4.model.Enumerations.AdministrativeGender; import org.hl7.fhir.r4.model.IdType; @@ -75,6 +78,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.in; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.startsWith; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -247,8 +251,8 @@ public class SystemProviderR4Test extends BaseJpaR4Test { @Test public void testMarkResourcesForReindexing() throws Exception { - HttpGet get = new HttpGet(ourServerBase + "/$mark-all-resources-for-reindexing"); - CloseableHttpResponse http = ourHttpClient.execute(get); + HttpRequestBase post = new HttpPost(ourServerBase + "/$mark-all-resources-for-reindexing"); + CloseableHttpResponse http = ourHttpClient.execute(post); try { String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(output); @@ -257,8 +261,8 @@ public class SystemProviderR4Test extends BaseJpaR4Test { IOUtils.closeQuietly(http); } - get = new HttpGet(ourServerBase + "/$perform-reindexing-pass"); - http = ourHttpClient.execute(get); + post = new HttpPost(ourServerBase + "/$perform-reindexing-pass"); + http = ourHttpClient.execute(post); try { String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(output); @@ -272,8 +276,10 @@ public class SystemProviderR4Test extends BaseJpaR4Test { @Test public void testMarkResourcesForReindexingTyped() throws Exception { - HttpGet get = new HttpGet(ourServerBase + "/$mark-all-resources-for-reindexing?type=Patient"); - CloseableHttpResponse http = ourHttpClient.execute(get); + + HttpPost post = new HttpPost(ourServerBase + "/$mark-all-resources-for-reindexing?type=Patient"); + post.setEntity(new ResourceEntity(myFhirCtx, new Parameters().addParameter("type", new CodeType("Patient")))); + CloseableHttpResponse http = ourHttpClient.execute(post); try { String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(output); @@ -282,8 +288,9 @@ public class SystemProviderR4Test extends BaseJpaR4Test { IOUtils.closeQuietly(http); } - get = new HttpGet(ourServerBase + "/$mark-all-resources-for-reindexing?type=FOO"); - http = ourHttpClient.execute(get); + post = new HttpPost(ourServerBase + "/$mark-all-resources-for-reindexing?type=FOO"); + post.setEntity(new ResourceEntity(myFhirCtx, new Parameters().addParameter("type", new CodeType("FOO")))); + http = ourHttpClient.execute(post); try { String output = IOUtils.toString(http.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(output); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/LastNElasticsearchSvcMultipleObservationsIT.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/LastNElasticsearchSvcMultipleObservationsIT.java index 8a837f94034..ae0bec25252 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/LastNElasticsearchSvcMultipleObservationsIT.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/search/lastn/LastNElasticsearchSvcMultipleObservationsIT.java @@ -29,16 +29,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.mock.mockito.MockBean; -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.testcontainers.elasticsearch.ElasticsearchContainer; import org.testcontainers.junit.jupiter.Container; @@ -66,20 +56,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class LastNElasticsearchSvcMultipleObservationsIT { static private final Calendar baseObservationDate = new GregorianCalendar(); - private static ObjectMapper ourMapperNonPrettyPrint; - - private static boolean indexLoaded = false; - - private final Map>> createdPatientObservationMap = new HashMap<>(); - - private final FhirContext myFhirContext = FhirContext.forCached(FhirVersionEnum.R4); - - @Container public static ElasticsearchContainer elasticsearchContainer = TestElasticsearchContainerHelper.getEmbeddedElasticSearch(); - - - + private static ObjectMapper ourMapperNonPrettyPrint; + private static boolean indexLoaded = false; + private final Map>> createdPatientObservationMap = new HashMap<>(); + private final FhirContext myFhirContext = FhirContext.forCached(FhirVersionEnum.R4); private ElasticsearchSvcImpl elasticsearchSvc; @BeforeEach diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java index fe5f13de317..cd8b745e224 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/stresstest/GiantTransactionPerfTest.java @@ -40,12 +40,10 @@ import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.validation.IInstanceValidatorModule; import com.google.common.collect.Lists; import org.hamcrest.Matchers; -import org.hibernate.Session; import org.hibernate.internal.SessionImpl; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.ExplanationOfBenefit; -import org.junit.Ignore; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImplTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImplTest.java index 45e7b561598..57f44b23eda 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImplTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/term/BaseTermReadSvcImplTest.java @@ -2,7 +2,7 @@ package ca.uhn.fhir.jpa.term; import org.junit.jupiter.api.Test; -import static org.junit.Assert.assertFalse; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class BaseTermReadSvcImplTest { diff --git a/hapi-fhir-jpaserver-batch/pom.xml b/hapi-fhir-jpaserver-batch/pom.xml index 6b49017689b..d3df9443653 100644 --- a/hapi-fhir-jpaserver-batch/pom.xml +++ b/hapi-fhir-jpaserver-batch/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-cql/pom.xml b/hapi-fhir-jpaserver-cql/pom.xml index 9be25f3da9e..8f5317c6e83 100644 --- a/hapi-fhir-jpaserver-cql/pom.xml +++ b/hapi-fhir-jpaserver-cql/pom.xml @@ -7,7 +7,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -131,6 +131,11 @@ + + org.testcontainers + junit-jupiter + test + org.mockito mockito-core @@ -144,13 +149,13 @@ ca.uhn.hapi.fhir hapi-fhir-test-utilities - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT test ca.uhn.hapi.fhir hapi-fhir-jpaserver-test-utilities - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT test diff --git a/hapi-fhir-jpaserver-cql/src/test/java/ca/uhn/fhir/cql/common/helper/TranslatorHelperTest.java b/hapi-fhir-jpaserver-cql/src/test/java/ca/uhn/fhir/cql/common/helper/TranslatorHelperTest.java index c4a9393ab75..ed94139951b 100644 --- a/hapi-fhir-jpaserver-cql/src/test/java/ca/uhn/fhir/cql/common/helper/TranslatorHelperTest.java +++ b/hapi-fhir-jpaserver-cql/src/test/java/ca/uhn/fhir/cql/common/helper/TranslatorHelperTest.java @@ -13,11 +13,8 @@ import org.cqframework.cql.cql2elm.NamespaceManager; import org.cqframework.cql.elm.execution.Library; import org.cqframework.cql.elm.tracking.TrackBack; import org.hl7.elm.r1.VersionedIdentifier; -import org.junit.After; -import org.junit.Before; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Matchers; @@ -33,8 +30,11 @@ import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import static org.junit.Assert.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.reset; @@ -61,7 +61,7 @@ public class TranslatorHelperTest implements CqlProviderTestBase { //@BeforeEach //@BeforeAll - @Before + @BeforeEach public void createMocks() { MockitoAnnotations.openMocks(this); //libraryManager = Mockito.mock(LibraryManager.class); @@ -85,7 +85,7 @@ public class TranslatorHelperTest implements CqlProviderTestBase { when(libraryManager.getNamespaceManager()).thenReturn(namespaceManager); when(namespaceManager.hasNamespaces()).thenReturn(false); CqlTranslator translator = TranslatorHelper.getTranslator(sampleCql, libraryManager, modelManager); - assertNotNull("translator should not be NULL!", translator); + assertNotNull(translator, "translator should not be NULL!"); } //@Test @@ -95,7 +95,7 @@ public class TranslatorHelperTest implements CqlProviderTestBase { when(libraryManager.getLibrarySourceLoader()).thenReturn(librarySourceLoader); when(namespaceManager.hasNamespaces()).thenReturn(true); CqlTranslator translator = TranslatorHelper.getTranslator(sampleCql, libraryManager, modelManager); - assertNotNull("translator should not be NULL!", translator); + assertNotNull(translator, "translator should not be NULL!"); } //@Test @@ -108,8 +108,8 @@ public class TranslatorHelperTest implements CqlProviderTestBase { try { translator = TranslatorHelper.getTranslator(" ", libraryManager, modelManager); fail(); - } catch(NullPointerException e) { - assertNull("translator should be NULL!", translator); + } catch (NullPointerException e) { + assertNull(translator, "translator should be NULL!"); } } @@ -133,9 +133,9 @@ public class TranslatorHelperTest implements CqlProviderTestBase { when(CqlTranslator.fromStream(any(InputStream.class), any(ModelManager.class), any(LibraryManager.class), Matchers.anyVararg())).thenThrow(IOException.class); translator = TranslatorHelper.getTranslator(new ByteArrayInputStream("INVALID-FILENAME".getBytes(StandardCharsets.UTF_8)), libraryManager, modelManager); fail(); - } catch(IllegalArgumentException | IOException e) { + } catch (IllegalArgumentException | IOException e) { assertTrue(e instanceof IllegalArgumentException); - assertNull("translator should be NULL!", translator); + assertNull(translator, "translator should be NULL!"); } } @@ -153,11 +153,11 @@ public class TranslatorHelperTest implements CqlProviderTestBase { Library library = null; try { library = TranslatorHelper.translateLibrary("INVALID-FILENAME", libraryManager, modelManager); - } catch(Exception e) { + } catch (Exception e) { e.printStackTrace(); fail(); } - assertNotNull("library should not be NULL!", library); + assertNotNull(library, "library should not be NULL!"); } @Test @@ -166,8 +166,8 @@ public class TranslatorHelperTest implements CqlProviderTestBase { try { library = TranslatorHelper.readLibrary(new ByteArrayInputStream("INVALID-XML-DOCUMENT".getBytes())); fail(); - } catch(IllegalArgumentException e) { - assertNull("library should be NULL!", library); + } catch (IllegalArgumentException e) { + assertNull(library, "library should be NULL!"); } } @@ -181,7 +181,7 @@ public class TranslatorHelperTest implements CqlProviderTestBase { } catch (IOException e) { e.printStackTrace(); } - assertNotNull("library should not be NULL!", library); + assertNotNull(library, "library should not be NULL!"); } @Test diff --git a/hapi-fhir-jpaserver-mdm/pom.xml b/hapi-fhir-jpaserver-mdm/pom.xml index 29d7f7a9503..f49c0de98fa 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -55,13 +55,13 @@ ca.uhn.hapi.fhir hapi-fhir-test-utilities - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT test ca.uhn.hapi.fhir hapi-fhir-jpaserver-test-utilities - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT test diff --git a/hapi-fhir-jpaserver-migrate/pom.xml b/hapi-fhir-jpaserver-migrate/pom.xml index be260a03b39..aa549ab2385 100644 --- a/hapi-fhir-jpaserver-migrate/pom.xml +++ b/hapi-fhir-jpaserver-migrate/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-model/pom.xml b/hapi-fhir-jpaserver-model/pom.xml index 7a40544f634..189bf7f38da 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-searchparam/pom.xml b/hapi-fhir-jpaserver-searchparam/pom.xml index 5bc11f73872..54c7f179c7b 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-subscription/pom.xml b/hapi-fhir-jpaserver-subscription/pom.xml index 369918d69c8..cd087657fa9 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-test-utilities/pom.xml b/hapi-fhir-jpaserver-test-utilities/pom.xml index 06b73aa7bac..5a330441e24 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 9cec5a13cf8..407bad660b7 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../pom.xml @@ -47,6 +47,11 @@ ${project.version} classes + + ca.uhn.hapi.fhir + hapi-fhir-server-openapi + ${project.version} + com.helger @@ -164,7 +169,7 @@ ca.uhn.hapi.fhir hapi-fhir-converter - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java index a7e2c06d36a..caa380403cd 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java @@ -19,6 +19,7 @@ import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; import ca.uhn.fhir.jpa.provider.r4.JpaSystemProviderR4; import ca.uhn.fhir.jpa.provider.r5.JpaSystemProviderR5; import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; +import ca.uhn.fhir.rest.openapi.OpenApiInterceptor; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.jpa.subscription.match.config.WebsocketDispatcherConfig; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; @@ -38,6 +39,7 @@ import ca.uhn.fhirtest.config.TestR4Config; import ca.uhn.fhirtest.config.TestR5Config; import ca.uhn.hapi.converters.server.VersionedApiConverterInterceptor; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -79,11 +81,11 @@ public class TestRestfulServer extends RestfulServer { WebApplicationContext parentAppCtx = ContextLoaderListener.getCurrentWebApplicationContext(); // These two parmeters are also declared in web.xml - String implDesc = getInitParameter("ImplementationDescription"); String fhirVersionParam = getInitParameter("FhirVersion"); - if (StringUtils.isBlank(fhirVersionParam)) { - fhirVersionParam = "DSTU1"; - } + Validate.notNull(fhirVersionParam); + + setImplementationDescription("HAPI FHIR Test/Demo Server " + fhirVersionParam + " Endpoint"); + setCopyright("This server is **Open Source Software**, licensed under the terms of the [Apache Software License 2.0](https://www.apache.org/licenses/LICENSE-2.0)."); // Depending on the version this server is supporing, we will // retrieve all the appropriate resource providers and the @@ -110,7 +112,6 @@ public class TestRestfulServer extends RestfulServer { systemDao = myAppCtx.getBean("mySystemDaoDstu2", IFhirSystemDao.class); etagSupport = ETagSupportEnum.ENABLED; JpaConformanceProviderDstu2 confProvider = new JpaConformanceProviderDstu2(this, systemDao, myAppCtx.getBean(DaoConfig.class)); - confProvider.setImplementationDescription(implDesc); setServerConformanceProvider(confProvider); break; } @@ -127,7 +128,6 @@ public class TestRestfulServer extends RestfulServer { systemDao = myAppCtx.getBean("mySystemDaoDstu3", IFhirSystemDao.class); etagSupport = ETagSupportEnum.ENABLED; JpaConformanceProviderDstu3 confProvider = new JpaConformanceProviderDstu3(this, systemDao, myAppCtx.getBean(DaoConfig.class), myAppCtx.getBean(ISearchParamRegistry.class)); - confProvider.setImplementationDescription(implDesc); setServerConformanceProvider(confProvider); providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class)); providers.add(myAppCtx.getBean(GraphQLProvider.class)); @@ -147,7 +147,6 @@ public class TestRestfulServer extends RestfulServer { etagSupport = ETagSupportEnum.ENABLED; IValidationSupport validationSupport = myAppCtx.getBean(IValidationSupport.class); JpaCapabilityStatementProvider confProvider = new JpaCapabilityStatementProvider(this, systemDao, myAppCtx.getBean(DaoConfig.class), myAppCtx.getBean(ISearchParamRegistry.class), validationSupport); - confProvider.setImplementationDescription(implDesc); setServerConformanceProvider(confProvider); providers.add(myAppCtx.getBean(TerminologyUploaderProvider.class)); providers.add(myAppCtx.getBean(GraphQLProvider.class)); @@ -271,6 +270,10 @@ public class TestRestfulServer extends RestfulServer { */ registerProvider(myAppCtx.getBean(DiffProvider.class)); + /* + * OpenAPI + */ + registerInterceptor(new OpenApiInterceptor()); } /** diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java index cbaf424d495..fd1b72bec5a 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu2Config.java @@ -109,7 +109,7 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 { retVal.setUsername(myDbUsername); retVal.setPassword(myDbPassword); retVal.setDefaultQueryTimeout(20); - retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE); + retVal.setTestOnBorrow(true); DataSource dataSource = ProxyDataSourceBuilder .create(retVal) diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java index bcbdb4a825f..a8d454a92e2 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestDstu3Config.java @@ -125,7 +125,7 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 { retVal.setUsername(myDbUsername); retVal.setPassword(myDbPassword); retVal.setDefaultQueryTimeout(20); - retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE); + retVal.setTestOnBorrow(true); DataSource dataSource = ProxyDataSourceBuilder .create(retVal) diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java index f1194ba835d..e0db7abf4a9 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR4Config.java @@ -109,7 +109,7 @@ public class TestR4Config extends BaseJavaConfigR4 { retVal.setUsername(myDbUsername); retVal.setPassword(myDbPassword); retVal.setDefaultQueryTimeout(20); - retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE); + retVal.setTestOnBorrow(true); DataSource dataSource = ProxyDataSourceBuilder .create(retVal) diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java index f7f4d08b241..5fc6e03cde4 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/TestR5Config.java @@ -109,7 +109,7 @@ public class TestR5Config extends BaseJavaConfigR5 { retVal.setUsername(myDbUsername); retVal.setPassword(myDbPassword); retVal.setDefaultQueryTimeout(20); - retVal.setMaxConnLifetimeMillis(5 * DateUtils.MILLIS_PER_MINUTE); + retVal.setTestOnBorrow(true); DataSource dataSource = ProxyDataSourceBuilder .create(retVal) diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml index 8fb6dbdfa9c..dd383fa7648 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml @@ -31,10 +31,6 @@ contextConfigLocation ca.uhn.fhirtest.config.FhirTesterConfig - 2 @@ -43,10 +39,6 @@ fhirServletR5 ca.uhn.fhirtest.TestRestfulServer - - ImplementationDescription - UHN Test Server (R5 Resources) - FhirVersion R5 @@ -57,10 +49,6 @@ fhirServletR4 ca.uhn.fhirtest.TestRestfulServer - - ImplementationDescription - UHN Test Server (R4 Resources) - FhirVersion R4 @@ -71,10 +59,6 @@ fhirServletDstu2 ca.uhn.fhirtest.TestRestfulServer - - ImplementationDescription - UHN Test Server (DSTU2 Resources) - FhirVersion DSTU2 @@ -85,10 +69,6 @@ fhirServletDstu3 ca.uhn.fhirtest.TestRestfulServer - - ImplementationDescription - UHN Test Server (STU3 Resources) - FhirVersion DSTU3 @@ -96,35 +76,6 @@ 1 - - fhirServletR5 /baseR5/* diff --git a/hapi-fhir-server-mdm/pom.xml b/hapi-fhir-server-mdm/pom.xml index 1952a50d819..213863947bf 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server-openapi/pom.xml b/hapi-fhir-server-openapi/pom.xml new file mode 100644 index 00000000000..59d85a5e228 --- /dev/null +++ b/hapi-fhir-server-openapi/pom.xml @@ -0,0 +1,78 @@ + + + + ca.uhn.hapi.fhir + hapi-deployable-pom + 5.4.0-PRE8-SNAPSHOT + ../hapi-deployable-pom/pom.xml + + + 4.0.0 + + hapi-fhir-server-openapi + + + + + ca.uhn.hapi.fhir + hapi-fhir-server + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-structures-r4 + ${project.version} + + + ca.uhn.hapi.fhir + hapi-fhir-converter + ${project.version} + + + + + io.swagger.core.v3 + swagger-models + 2.1.7 + + + io.swagger.core.v3 + swagger-core + + + org.webjars + swagger-ui + + + + + org.thymeleaf + thymeleaf + + + com.vladsch.flexmark + flexmark + + + + + ca.uhn.hapi.fhir + hapi-fhir-test-utilities + ${project.version} + + + ch.qos.logback + logback-classic + test + + + net.sourceforge.htmlunit + htmlunit + test + + + + + diff --git a/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java b/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java new file mode 100644 index 00000000000..5d0dfe7c0e3 --- /dev/null +++ b/hapi-fhir-server-openapi/src/main/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptor.java @@ -0,0 +1,903 @@ +package ca.uhn.fhir.rest.openapi; + +/*- + * #%L + * hapi-fhir-server-openapi + * %% + * Copyright (C) 2014 - 2021 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.context.FhirVersionEnum; +import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.server.IServerAddressStrategy; +import ca.uhn.fhir.rest.server.IServerConformanceProvider; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.RestfulServerUtils; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.util.ClasspathUtil; +import ca.uhn.fhir.util.ExtensionConstants; +import ca.uhn.fhir.util.HapiExtensions; +import ca.uhn.fhir.util.UrlUtil; +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.examples.Example; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.ObjectSchema; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.parameters.RequestBody; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import io.swagger.v3.oas.models.servers.Server; +import io.swagger.v3.oas.models.tags.Tag; +import org.apache.commons.io.IOUtils; +import org.hl7.fhir.convertors.VersionConvertor_30_40; +import org.hl7.fhir.convertors.VersionConvertor_40_50; +import org.hl7.fhir.instance.model.api.IBaseConformance; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.CapabilityStatement; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.OperationDefinition; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.Resource; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.Type; +import org.thymeleaf.IEngineConfiguration; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.cache.AlwaysValidCacheEntryValidity; +import org.thymeleaf.cache.ICacheEntryValidity; +import org.thymeleaf.cache.NonCacheableCacheEntryValidity; +import org.thymeleaf.context.IExpressionContext; +import org.thymeleaf.context.WebContext; +import org.thymeleaf.linkbuilder.AbstractLinkBuilder; +import org.thymeleaf.standard.StandardDialect; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.ITemplateResolver; +import org.thymeleaf.templateresolver.TemplateResolution; +import org.thymeleaf.templateresource.ClassLoaderTemplateResource; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; +import static org.apache.commons.lang3.StringUtils.defaultString; +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +public class OpenApiInterceptor { + + public static final String FHIR_JSON_RESOURCE = "FHIR-JSON-RESOURCE"; + public static final String FHIR_XML_RESOURCE = "FHIR-XML-RESOURCE"; + public static final String PAGE_SYSTEM = "System Level Operations"; + public static final String PAGE_ALL = "All"; + public static final FhirContext FHIR_CONTEXT_CANONICAL = FhirContext.forR4(); + public static final String REQUEST_DETAILS = "REQUEST_DETAILS"; + public static final String RACCOON_PNG = "raccoon.png"; + private final String mySwaggerUiVersion; + private final TemplateEngine myTemplateEngine; + private final Parser myFlexmarkParser; + private final HtmlRenderer myFlexmarkRenderer; + private final Map myResourcePathToClasspath = new HashMap<>(); + private final Map myExtensionToContentType = new HashMap<>(); + private String myBannerImage; + + /** + * Constructor + */ + public OpenApiInterceptor() { + mySwaggerUiVersion = initSwaggerUiWebJar(); + + myTemplateEngine = new TemplateEngine(); + ITemplateResolver resolver = new SwaggerUiTemplateResolver(); + myTemplateEngine.setTemplateResolver(resolver); + StandardDialect dialect = new StandardDialect(); + myTemplateEngine.setDialect(dialect); + + myTemplateEngine.setLinkBuilder(new TemplateLinkBuilder()); + + myFlexmarkParser = Parser.builder().build(); + myFlexmarkRenderer = HtmlRenderer.builder().build(); + + initResources(); + } + + private void initResources() { + setBannerImage(RACCOON_PNG); + + addResourcePathToClasspath("/swagger-ui/index.html", "/ca/uhn/fhir/rest/openapi/index.html"); + addResourcePathToClasspath("/swagger-ui/" + RACCOON_PNG, "/ca/uhn/fhir/rest/openapi/raccoon.png"); + addResourcePathToClasspath("/swagger-ui/index.css", "/ca/uhn/fhir/rest/openapi/index.css"); + + myExtensionToContentType.put(".png", "image/png"); + myExtensionToContentType.put(".css", "text/css; charset=UTF-8"); + } + + protected void addResourcePathToClasspath(String thePath, String theClasspath) { + myResourcePathToClasspath.put(thePath, theClasspath); + } + + private String initSwaggerUiWebJar() { + final String mySwaggerUiVersion; + Properties props = new Properties(); + String resourceName = "/META-INF/maven/org.webjars/swagger-ui/pom.properties"; + try { + InputStream resourceAsStream = ClasspathUtil.loadResourceAsStream(resourceName); + props.load(resourceAsStream); + } catch (IOException e) { + throw new ConfigurationException("Failed to load resource: " + resourceName); + } + mySwaggerUiVersion = props.getProperty("version"); + return mySwaggerUiVersion; + } + + @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLER_SELECTED) + public boolean serveSwaggerUi(HttpServletRequest theRequest, HttpServletResponse theResponse, ServletRequestDetails theRequestDetails) throws IOException { + String requestPath = theRequest.getPathInfo(); + + if (isBlank(requestPath) || requestPath.equals("/")) { + Set highestRankedAcceptValues = RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theRequest); + if (highestRankedAcceptValues.contains(Constants.CT_HTML)) { + theResponse.sendRedirect("./swagger-ui/"); + return false; + } + + } + + if (requestPath.startsWith("/swagger-ui/")) { + + return !handleResourceRequest(theResponse, theRequestDetails, requestPath); + + } else if (requestPath.equals("/api-docs")) { + + OpenAPI openApi = generateOpenApi(theRequestDetails); + String response = Yaml.pretty(openApi); + + theResponse.setContentType("text/yaml"); + theResponse.setStatus(200); + theResponse.getWriter().write(response); + theResponse.getWriter().close(); + return false; + + } + + return true; + } + + protected boolean handleResourceRequest(HttpServletResponse theResponse, ServletRequestDetails theRequestDetails, String requestPath) throws IOException { + if (requestPath.equals("/swagger-ui/") || requestPath.equals("/swagger-ui/index.html")) { + serveSwaggerUiHtml(theRequestDetails, theResponse); + return true; + } + + String resourceClasspath = myResourcePathToClasspath.get(requestPath); + if (resourceClasspath != null) { + theResponse.setStatus(200); + + String extension = requestPath.substring(requestPath.lastIndexOf('.')); + String contentType = myExtensionToContentType.get(extension); + assert contentType != null; + theResponse.setContentType(contentType); + try (InputStream resource = ClasspathUtil.loadResourceAsStream(resourceClasspath)) { + IOUtils.copy(resource, theResponse.getOutputStream()); + theResponse.getOutputStream().close(); + } + return true; + } + + + String resourcePath = requestPath.substring("/swagger-ui/".length()); + try (InputStream resource = ClasspathUtil.loadResourceAsStream("/META-INF/resources/webjars/swagger-ui/" + mySwaggerUiVersion + "/" + resourcePath)) { + + if (resourcePath.endsWith(".js") || resourcePath.endsWith(".map")) { + theResponse.setContentType("application/javascript"); + theResponse.setStatus(200); + IOUtils.copy(resource, theResponse.getOutputStream()); + theResponse.getOutputStream().close(); + return true; + } + + if (resourcePath.endsWith(".css")) { + theResponse.setContentType("text/css"); + theResponse.setStatus(200); + IOUtils.copy(resource, theResponse.getOutputStream()); + theResponse.getOutputStream().close(); + return true; + } + + } + return false; + } + + @SuppressWarnings("unchecked") + private void serveSwaggerUiHtml(ServletRequestDetails theRequestDetails, HttpServletResponse theResponse) throws IOException { + CapabilityStatement cs = getCapabilityStatement(theRequestDetails); + + theResponse.setStatus(200); + theResponse.setContentType(Constants.CT_HTML); + + HttpServletRequest servletRequest = theRequestDetails.getServletRequest(); + ServletContext servletContext = servletRequest.getServletContext(); + WebContext context = new WebContext(servletRequest, theResponse, servletContext); + context.setVariable(REQUEST_DETAILS, theRequestDetails); + context.setVariable("DESCRIPTION", cs.getImplementation().getDescription()); + context.setVariable("SERVER_NAME", cs.getSoftware().getName()); + context.setVariable("SERVER_VERSION", cs.getSoftware().getVersion()); + context.setVariable("BASE_URL", cs.getImplementation().getUrl()); + context.setVariable("BANNER_IMAGE_URL", getBannerImage()); + context.setVariable("OPENAPI_DOCS", cs.getImplementation().getUrl() + "/api-docs"); + context.setVariable("FHIR_VERSION", cs.getFhirVersion().toCode()); + context.setVariable("FHIR_VERSION_CODENAME", FhirVersionEnum.forVersionString(cs.getFhirVersion().toCode()).name()); + + String copyright = cs.getCopyright(); + if (isNotBlank(copyright)) { + copyright = myFlexmarkRenderer.render(myFlexmarkParser.parse(copyright)); + context.setVariable("COPYRIGHT_HTML", copyright); + } + + List pageNames = new ArrayList<>(); + Map resourceToCount = new HashMap<>(); + cs.getRestFirstRep().getResource().stream().forEach(t -> { + String type = t.getType(); + pageNames.add(type); + Extension countExtension = t.getExtensionByUrl(ExtensionConstants.CONF_RESOURCE_COUNT); + if (countExtension != null) { + IPrimitiveType countExtensionValue = (IPrimitiveType) countExtension.getValueAsPrimitive(); + if (countExtensionValue != null && countExtensionValue.hasValue()) { + resourceToCount.put(type, countExtensionValue.getValue().intValue()); + } + } + }); + pageNames.sort((o1, o2) -> { + Integer count1 = resourceToCount.get(o1); + Integer count2 = resourceToCount.get(o2); + if (count1 != null && count2 != null) { + return count2 - count1; + } + if (count1 != null) { + return -1; + } + if (count2 != null) { + return 1; + } + return o1.compareTo(o2); + }); + + pageNames.add(0, PAGE_ALL); + pageNames.add(1, PAGE_SYSTEM); + + context.setVariable("PAGE_NAMES", pageNames); + context.setVariable("PAGE_NAME_TO_COUNT", resourceToCount); + + String page = extractPageName(theRequestDetails, PAGE_SYSTEM); + context.setVariable("PAGE", page); + + String outcome = myTemplateEngine.process("index.html", context); + + theResponse.getWriter().write(outcome); + theResponse.getWriter().close(); + } + + private String extractPageName(ServletRequestDetails theRequestDetails, String theDefault) { + String[] pageValues = theRequestDetails.getParameters().get("page"); + String page = null; + if (pageValues != null && pageValues.length > 0) { + page = pageValues[0]; + } + if (isBlank(page)) { + page = theDefault; + } + return page; + } + + private OpenAPI generateOpenApi(ServletRequestDetails theRequestDetails) { + String page = extractPageName(theRequestDetails, null); + + CapabilityStatement cs = getCapabilityStatement(theRequestDetails); + FhirContext ctx = theRequestDetails.getFhirContext(); + + IServerConformanceProvider capabilitiesProvider = null; + RestfulServer restfulServer = theRequestDetails.getServer(); + if (restfulServer.getServerConformanceProvider() instanceof IServerConformanceProvider) { + capabilitiesProvider = (IServerConformanceProvider) restfulServer.getServerConformanceProvider(); + } + + + OpenAPI openApi = new OpenAPI(); + + openApi.setInfo(new Info()); + openApi.getInfo().setDescription(cs.getDescription()); + openApi.getInfo().setTitle(cs.getSoftware().getName()); + openApi.getInfo().setVersion(cs.getSoftware().getVersion()); + openApi.getInfo().setContact(new Contact()); + openApi.getInfo().getContact().setName(cs.getContactFirstRep().getName()); + openApi.getInfo().getContact().setEmail(cs.getContactFirstRep().getTelecomFirstRep().getValue()); + + Server server = new Server(); + openApi.addServersItem(server); + server.setUrl(cs.getImplementation().getUrl()); + server.setDescription(cs.getSoftware().getName()); + + Paths paths = new Paths(); + openApi.setPaths(paths); + + if (page == null || page.equals(PAGE_SYSTEM) || page.equals(PAGE_ALL)) { + Tag serverTag = new Tag(); + serverTag.setName(PAGE_SYSTEM); + serverTag.setDescription("Server-level operations"); + openApi.addTagsItem(serverTag); + + Operation capabilitiesOperation = getPathItem(paths, "/metadata", PathItem.HttpMethod.GET); + capabilitiesOperation.addTagsItem(PAGE_SYSTEM); + capabilitiesOperation.setSummary("server-capabilities: Fetch the server FHIR CapabilityStatement"); + addFhirResourceResponse(ctx, openApi, capabilitiesOperation, "CapabilityStatement"); + + Set systemInteractions = cs.getRestFirstRep().getInteraction().stream().map(t -> t.getCode()).collect(Collectors.toSet()); + + // Transaction Operation + if (systemInteractions.contains(CapabilityStatement.SystemRestfulInteraction.TRANSACTION) || systemInteractions.contains(CapabilityStatement.SystemRestfulInteraction.BATCH)) { + Operation transaction = getPathItem(paths, "/", PathItem.HttpMethod.POST); + transaction.addTagsItem(PAGE_SYSTEM); + transaction.setSummary("server-transaction: Execute a FHIR Transaction (or FHIR Batch) Bundle"); + addFhirResourceResponse(ctx, openApi, transaction, null); + addFhirResourceRequestBody(openApi, transaction, ctx, null); + } + + // System History Operation + if (systemInteractions.contains(CapabilityStatement.SystemRestfulInteraction.HISTORYSYSTEM)) { + Operation systemHistory = getPathItem(paths, "/_history", PathItem.HttpMethod.GET); + systemHistory.addTagsItem(PAGE_SYSTEM); + systemHistory.setSummary("server-history: Fetch the resource change history across all resource types on the server"); + addFhirResourceResponse(ctx, openApi, systemHistory, null); + } + + // System-level Operations + for (CapabilityStatement.CapabilityStatementRestResourceOperationComponent nextOperation : cs.getRestFirstRep().getOperation()) { + addFhirOperation(ctx, openApi, theRequestDetails, capabilitiesProvider, paths, null, nextOperation); + } + + } + + for (CapabilityStatement.CapabilityStatementRestResourceComponent nextResource : cs.getRestFirstRep().getResource()) { + String resourceType = nextResource.getType(); + + if (page != null && !page.equals(resourceType) && !page.equals(PAGE_ALL)) { + continue; + } + + Set typeRestfulInteractions = nextResource.getInteraction().stream().map(t -> t.getCodeElement().getValue()).collect(Collectors.toSet()); + + Tag resourceTag = new Tag(); + resourceTag.setName(resourceType); + resourceTag.setDescription("The " + resourceType + " FHIR resource type"); + openApi.addTagsItem(resourceTag); + + // Instance Read + if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.READ)) { + Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.GET); + operation.addTagsItem(resourceType); + operation.setSummary("read-instance: Read " + resourceType + " instance"); + addResourceIdParameter(operation); + addFhirResourceResponse(ctx, openApi, operation, null); + } + + // Instance VRead + if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.VREAD)) { + Operation operation = getPathItem(paths, "/" + resourceType + "/{id}/_history/{version_id}", PathItem.HttpMethod.GET); + operation.addTagsItem(resourceType); + operation.setSummary("vread-instance: Read " + resourceType + " instance with specific version"); + addResourceIdParameter(operation); + addResourceVersionIdParameter(operation); + addFhirResourceResponse(ctx, openApi, operation, null); + } + + // Type Create + if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.CREATE)) { + Operation operation = getPathItem(paths, "/" + resourceType, PathItem.HttpMethod.POST); + operation.addTagsItem(resourceType); + operation.setSummary("create-type: Create a new " + resourceType + " instance"); + addFhirResourceRequestBody(openApi, operation, ctx, genericExampleSupplier(ctx, resourceType)); + addFhirResourceResponse(ctx, openApi, operation, null); + } + + // Instance Update + if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.UPDATE)) { + Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.PUT); + operation.addTagsItem(resourceType); + operation.setSummary("update-instance: Update an existing " + resourceType + " instance, or create using a client-assigned ID"); + addResourceIdParameter(operation); + addFhirResourceRequestBody(openApi, operation, ctx, genericExampleSupplier(ctx, resourceType)); + addFhirResourceResponse(ctx, openApi, operation, null); + } + + // Type history + if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.HISTORYTYPE)) { + Operation operation = getPathItem(paths, "/" + resourceType + "/_history", PathItem.HttpMethod.GET); + operation.addTagsItem(resourceType); + operation.setSummary("type-history: Fetch the resource change history for all resources of type " + resourceType); + addFhirResourceResponse(ctx, openApi, operation, null); + } + + // Instance history + if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.HISTORYTYPE)) { + Operation operation = getPathItem(paths, "/" + resourceType + "/{id}/_history", PathItem.HttpMethod.GET); + operation.addTagsItem(resourceType); + operation.setSummary("instance-history: Fetch the resource change history for all resources of type " + resourceType); + addResourceIdParameter(operation); + addFhirResourceResponse(ctx, openApi, operation, null); + } + + // Instance Patch + if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.PATCH)) { + Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.PATCH); + operation.addTagsItem(resourceType); + operation.setSummary("instance-patch: Patch a resource instance of type " + resourceType + " by ID"); + addResourceIdParameter(operation); + addFhirResourceRequestBody(openApi, operation, FHIR_CONTEXT_CANONICAL, patchExampleSupplier()); + addFhirResourceResponse(ctx, openApi, operation, null); + } + + // Instance Delete + if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.DELETE)) { + Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.DELETE); + operation.addTagsItem(resourceType); + operation.setSummary("instance-delete: Perform a logical delete on a resource instance"); + addResourceIdParameter(operation); + addFhirResourceResponse(ctx, openApi, operation, null); + } + + // Search + if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.SEARCHTYPE)) { + Operation operation = getPathItem(paths, "/" + resourceType, PathItem.HttpMethod.GET); + operation.addTagsItem(resourceType); + operation.setDescription("This is a search type"); + operation.setSummary("search-type: Update an existing " + resourceType + " instance, or create using a client-assigned ID"); + addFhirResourceResponse(ctx, openApi, operation, null); + + for (CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent nextSearchParam : nextResource.getSearchParam()) { + Parameter parametersItem = new Parameter(); + operation.addParametersItem(parametersItem); + + parametersItem.setName(nextSearchParam.getName()); + parametersItem.setIn("query"); + parametersItem.setDescription(nextSearchParam.getDocumentation()); + parametersItem.setStyle(Parameter.StyleEnum.SIMPLE); + } + } + + // Resource-level Operations + for (CapabilityStatement.CapabilityStatementRestResourceOperationComponent nextOperation : nextResource.getOperation()) { + addFhirOperation(ctx, openApi, theRequestDetails, capabilitiesProvider, paths, resourceType, nextOperation); + } + + } + + return openApi; + } + + private Supplier patchExampleSupplier() { + return () -> { + Parameters example = new Parameters(); + Parameters.ParametersParameterComponent operation = example + .addParameter() + .setName("operation"); + operation.addPart().setName("type").setValue(new StringType("add")); + operation.addPart().setName("path").setValue(new StringType("Patient")); + operation.addPart().setName("name").setValue(new StringType("birthDate")); + operation.addPart().setName("value").setValue(new DateType("1930-01-01")); + return example; + }; + } + + private void addSchemaFhirResource(OpenAPI theOpenApi) { + ensureComponentsSchemasPopulated(theOpenApi); + + if (!theOpenApi.getComponents().getSchemas().containsKey(FHIR_JSON_RESOURCE)) { + ObjectSchema fhirJsonSchema = new ObjectSchema(); + fhirJsonSchema.setDescription("A FHIR resource"); + theOpenApi.getComponents().addSchemas(FHIR_JSON_RESOURCE, fhirJsonSchema); + } + + if (!theOpenApi.getComponents().getSchemas().containsKey(FHIR_XML_RESOURCE)) { + ObjectSchema fhirXmlSchema = new ObjectSchema(); + fhirXmlSchema.setDescription("A FHIR resource"); + theOpenApi.getComponents().addSchemas(FHIR_XML_RESOURCE, fhirXmlSchema); + } + } + + private void ensureComponentsSchemasPopulated(OpenAPI theOpenApi) { + if (theOpenApi.getComponents() == null) { + theOpenApi.setComponents(new Components()); + } + if (theOpenApi.getComponents().getSchemas() == null) { + theOpenApi.getComponents().setSchemas(new LinkedHashMap<>()); + } + } + + private CapabilityStatement getCapabilityStatement(ServletRequestDetails theRequestDetails) { + RestfulServer restfulServer = theRequestDetails.getServer(); + IBaseConformance versionIndependentCapabilityStatement = restfulServer.getCapabilityStatement(theRequestDetails); + return toCanonicalVersion(versionIndependentCapabilityStatement); + } + + private void addFhirOperation(FhirContext theFhirContext, OpenAPI theOpenApi, ServletRequestDetails theRequestDetails, IServerConformanceProvider theCapabilitiesProvider, Paths thePaths, String theResourceType, CapabilityStatement.CapabilityStatementRestResourceOperationComponent theOperation) { + if (theCapabilitiesProvider != null) { + IdType definitionId = new IdType(theOperation.getDefinition()); + IBaseResource operationDefinitionNonCanonical = theCapabilitiesProvider.readOperationDefinition(definitionId, theRequestDetails); + if (operationDefinitionNonCanonical == null) { + return; + } + + OperationDefinition operationDefinition = toCanonicalVersion(operationDefinitionNonCanonical); + + if (!operationDefinition.getAffectsState()) { + + // GET form for non-state-affecting operations + if (theResourceType != null) { + if (operationDefinition.getType()) { + Operation operation = getPathItem(thePaths, "/" + theResourceType + "/$" + operationDefinition.getCode(), PathItem.HttpMethod.GET); + populateOperation(theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, true); + } + if (operationDefinition.getInstance()) { + Operation operation = getPathItem(thePaths, "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(), PathItem.HttpMethod.GET); + addResourceIdParameter(operation); + populateOperation(theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, true); + } + } else { + if (operationDefinition.getSystem()) { + Operation operation = getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.GET); + populateOperation(theFhirContext, theOpenApi, null, operationDefinition, operation, true); + } + } + + } else { + + // POST form for all operations + if (theResourceType != null) { + if (operationDefinition.getType()) { + Operation operation = getPathItem(thePaths, "/" + theResourceType + "/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST); + populateOperation(theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, false); + } + if (operationDefinition.getInstance()) { + Operation operation = getPathItem(thePaths, "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST); + addResourceIdParameter(operation); + populateOperation(theFhirContext, theOpenApi, theResourceType, operationDefinition, operation, false); + } + } else { + if (operationDefinition.getSystem()) { + Operation operation = getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST); + populateOperation(theFhirContext, theOpenApi, null, operationDefinition, operation, false); + } + } + + } + } + } + + private void populateOperation(FhirContext theFhirContext, OpenAPI theOpenApi, String theResourceType, OperationDefinition theOperationDefinition, Operation theOperation, boolean theGet) { + if (theResourceType == null) { + theOperation.addTagsItem(PAGE_SYSTEM); + } else { + theOperation.addTagsItem(theResourceType); + } + theOperation.setSummary(theOperationDefinition.getTitle()); + theOperation.setDescription(theOperationDefinition.getDescription()); + addFhirResourceResponse(theFhirContext, theOpenApi, theOperation, null); + + if (theGet) { + + for (OperationDefinition.OperationDefinitionParameterComponent nextParameter : theOperationDefinition.getParameter()) { + Parameter parametersItem = new Parameter(); + theOperation.addParametersItem(parametersItem); + + parametersItem.setName(nextParameter.getName()); + parametersItem.setIn("query"); + parametersItem.setDescription(nextParameter.getDocumentation()); + parametersItem.setStyle(Parameter.StyleEnum.SIMPLE); + parametersItem.setRequired(nextParameter.getMin() > 0); + + List exampleExtensions = nextParameter.getExtensionsByUrl(HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE); + if (exampleExtensions.size() == 1) { + parametersItem.setExample(exampleExtensions.get(0).getValueAsPrimitive().getValueAsString()); + } else if (exampleExtensions.size() > 1) { + for (Extension next : exampleExtensions) { + String nextExample = next.getValueAsPrimitive().getValueAsString(); + parametersItem.addExample(nextExample, new Example().value(nextExample)); + } + } + + } + + } else { + + Parameters exampleRequestBody = new Parameters(); + for (OperationDefinition.OperationDefinitionParameterComponent nextSearchParam : theOperationDefinition.getParameter()) { + Parameters.ParametersParameterComponent param = exampleRequestBody.addParameter(); + param.setName(nextSearchParam.getName()); + String paramType = nextSearchParam.getType(); + switch (defaultString(paramType)) { + case "uri": + case "url": + case "code": + case "string": { + IPrimitiveType type = (IPrimitiveType) FHIR_CONTEXT_CANONICAL.getElementDefinition(paramType).newInstance(); + type.setValueAsString("example"); + param.setValue((Type) type); + break; + } + case "integer": { + IPrimitiveType type = (IPrimitiveType) FHIR_CONTEXT_CANONICAL.getElementDefinition(paramType).newInstance(); + type.setValueAsString("0"); + param.setValue((Type) type); + break; + } + case "boolean": { + IPrimitiveType type = (IPrimitiveType) FHIR_CONTEXT_CANONICAL.getElementDefinition(paramType).newInstance(); + type.setValueAsString("false"); + param.setValue((Type) type); + break; + } + case "CodeableConcept": { + CodeableConcept type = new CodeableConcept(); + type.getCodingFirstRep().setSystem("http://example.com"); + type.getCodingFirstRep().setCode("1234"); + param.setValue(type); + break; + } + case "Coding": { + Coding type = new Coding(); + type.setSystem("http://example.com"); + type.setCode("1234"); + param.setValue(type); + break; + } + case "Reference": + Reference reference = new Reference("example"); + param.setValue(reference); + break; + case "Resource": + if (theResourceType != null) { + IBaseResource resource = FHIR_CONTEXT_CANONICAL.getResourceDefinition(theResourceType).newInstance(); + resource.setId("1"); + param.setResource((Resource) resource); + } + break; + } + + } + + String exampleRequestBodyString = FHIR_CONTEXT_CANONICAL.newJsonParser().setPrettyPrint(true).encodeResourceToString(exampleRequestBody); + theOperation.setRequestBody(new RequestBody()); + theOperation.getRequestBody().setContent(new Content()); + MediaType mediaType = new MediaType(); + mediaType.setExample(exampleRequestBodyString); + mediaType.setSchema(new Schema().type("object").title("FHIR Resource")); + theOperation.getRequestBody().getContent().addMediaType(Constants.CT_FHIR_JSON_NEW, mediaType); + + + } + } + + private Operation getPathItem(Paths thePaths, String thePath, PathItem.HttpMethod theMethod) { + PathItem pathItem; + if (thePaths.containsKey(thePath)) { + pathItem = thePaths.get(thePath); + } else { + pathItem = new PathItem(); + thePaths.addPathItem(thePath, pathItem); + } + + switch (theMethod) { + case POST: + assert pathItem.getPost() == null : "Have duplicate POST at path: " + thePath; + return pathItem.post(new Operation()).getPost(); + case GET: + assert pathItem.getGet() == null : "Have duplicate GET at path: " + thePath; + return pathItem.get(new Operation()).getGet(); + case PUT: + assert pathItem.getPut() == null; + return pathItem.put(new Operation()).getPut(); + case PATCH: + assert pathItem.getPatch() == null; + return pathItem.patch(new Operation()).getPatch(); + case DELETE: + assert pathItem.getDelete() == null; + return pathItem.delete(new Operation()).getDelete(); + case HEAD: + case OPTIONS: + case TRACE: + default: + throw new IllegalStateException(); + } + } + + private void addFhirResourceRequestBody(OpenAPI theOpenApi, Operation theOperation, FhirContext theExampleFhirContext, Supplier theExampleSupplier) { + RequestBody requestBody = new RequestBody(); + requestBody.setContent(provideContentFhirResource(theOpenApi, theExampleFhirContext, theExampleSupplier)); + theOperation.setRequestBody(requestBody); + } + + private void addResourceVersionIdParameter(Operation theOperation) { + Parameter parameter = new Parameter(); + parameter.setName("version_id"); + parameter.setIn("path"); + parameter.setDescription("The resource version ID"); + parameter.setExample("1"); + parameter.setSchema(new Schema().type("string").minimum(new BigDecimal(1))); + parameter.setStyle(Parameter.StyleEnum.SIMPLE); + theOperation.addParametersItem(parameter); + } + + private void addFhirResourceResponse(FhirContext theFhirContext, OpenAPI theOpenApi, Operation theOperation, String theResourceType) { + theOperation.setResponses(new ApiResponses()); + ApiResponse response200 = new ApiResponse(); + response200.setDescription("Success"); + response200.setContent(provideContentFhirResource(theOpenApi, theFhirContext, genericExampleSupplier(theFhirContext, theResourceType))); + theOperation.getResponses().addApiResponse("200", response200); + } + + private Supplier genericExampleSupplier(FhirContext theFhirContext, String theResourceType) { + if (theResourceType == null) { + return null; + } + return () -> { + IBaseResource example = null; + if (theResourceType != null) { + example = theFhirContext.getResourceDefinition(theResourceType).newInstance(); + } + return example; + }; + } + + + private Content provideContentFhirResource(OpenAPI theOpenApi, FhirContext theExampleFhirContext, Supplier theExampleSupplier) { + addSchemaFhirResource(theOpenApi); + Content retVal = new Content(); + + MediaType jsonSchema = new MediaType().schema(new ObjectSchema().$ref("#/components/schemas/" + FHIR_JSON_RESOURCE)); + if (theExampleSupplier != null) { + jsonSchema.setExample(theExampleFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(theExampleSupplier.get())); + } + retVal.addMediaType(Constants.CT_FHIR_JSON_NEW, jsonSchema); + + MediaType xmlSchema = new MediaType().schema(new ObjectSchema().$ref("#/components/schemas/" + FHIR_XML_RESOURCE)); + if (theExampleSupplier != null) { + xmlSchema.setExample(theExampleFhirContext.newXmlParser().setPrettyPrint(true).encodeResourceToString(theExampleSupplier.get())); + } + retVal.addMediaType(Constants.CT_FHIR_XML_NEW, xmlSchema); + return retVal; + } + + private void addResourceIdParameter(Operation theOperation) { + Parameter parameter = new Parameter(); + parameter.setName("id"); + parameter.setIn("path"); + parameter.setDescription("The resource ID"); + parameter.setExample("123"); + parameter.setSchema(new Schema().type("string").minimum(new BigDecimal(1))); + parameter.setStyle(Parameter.StyleEnum.SIMPLE); + theOperation.addParametersItem(parameter); + } + + protected ClassLoaderTemplateResource getIndexTemplate() { + return new ClassLoaderTemplateResource(myResourcePathToClasspath.get("/swagger-ui/index.html"), StandardCharsets.UTF_8.name()); + } + + public void setBannerImage(String theBannerImage) { + myBannerImage = theBannerImage; + } + + public String getBannerImage() { + return myBannerImage; + } + + private class SwaggerUiTemplateResolver implements ITemplateResolver { + @Override + public String getName() { + return getClass().getName(); + } + + @Override + public Integer getOrder() { + return 0; + } + + @Override + public TemplateResolution resolveTemplate(IEngineConfiguration configuration, String ownerTemplate, String template, Map templateResolutionAttributes) { + ClassLoaderTemplateResource resource = getIndexTemplate(); + ICacheEntryValidity cacheValidity = new AlwaysValidCacheEntryValidity(); + return new TemplateResolution(resource, TemplateMode.HTML, cacheValidity); + } + } + + private static class TemplateLinkBuilder extends AbstractLinkBuilder { + + @Override + public String buildLink(IExpressionContext theExpressionContext, String theBase, Map theParameters) { + + ServletRequestDetails requestDetails = (ServletRequestDetails) theExpressionContext.getVariable(REQUEST_DETAILS); + + IServerAddressStrategy addressStrategy = requestDetails.getServer().getServerAddressStrategy(); + String baseUrl = addressStrategy.determineServerBase(requestDetails.getServletRequest().getServletContext(), requestDetails.getServletRequest()); + + StringBuilder builder = new StringBuilder(); + builder.append(baseUrl); + builder.append(theBase); + if (!theParameters.isEmpty()) { + builder.append("?"); + for (Iterator> iter = theParameters.entrySet().iterator(); iter.hasNext(); ) { + Map.Entry nextEntry = iter.next(); + builder.append(UrlUtil.escapeUrlParam(nextEntry.getKey())); + builder.append("="); + builder.append(UrlUtil.escapeUrlParam(defaultIfNull(nextEntry.getValue(), "").toString())); + if (iter.hasNext()) { + builder.append("&"); + } + } + } + + return builder.toString(); + } + } + + @SuppressWarnings("unchecked") + private static T toCanonicalVersion(IBaseResource theNonCanonical) { + IBaseResource canonical; + if (theNonCanonical instanceof org.hl7.fhir.dstu3.model.Resource) { + canonical = VersionConvertor_30_40.convertResource((org.hl7.fhir.dstu3.model.Resource) theNonCanonical, true); + } else if (theNonCanonical instanceof org.hl7.fhir.r5.model.Resource) { + canonical = VersionConvertor_40_50.convertResource((org.hl7.fhir.r5.model.Resource) theNonCanonical); + } else { + canonical = theNonCanonical; + } + return (T) canonical; + } + + +} diff --git a/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.css b/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.css new file mode 100644 index 00000000000..a94d230ed3c --- /dev/null +++ b/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.css @@ -0,0 +1,141 @@ +html +{ + box-sizing: border-box; + overflow: -moz-scrollbars-vertical; + overflow-y: scroll; +} + +*, +*:before, +*:after +{ + box-sizing: inherit; +} + +body +{ + margin:0; + background: #fafafa; +} + +.scheme-container, .information-container +{ + display: none +} + +.banner { + padding-top: 20px; + padding-left: 25px; + display: flex; + flex-direction: row; + background-color: #AAA; + border-bottom: 2px solid #888; +} + +.banner H1 { + font-family: sans-serif; + height: 150%; + position: relative; + line-height: 120%; + top: 6px; + left: 18px; +} + +.banner .version { + font-size: 30%; + position: relative; + top: -11px; + background-color: #888; + color: #FFF; + padding: 5px; + border-radius: 15px; +} + +.bannerCopyright { + font-family: sans-serif; + padding-top: 20px; + padding-left: 30px; + padding-right: 30px; + padding-bottom: 20px; + background-color: #CCC; + border-bottom: 2px solid #888; + width: 100%; + font-size: 0.9em; +} + +.banner2 { + font-family: sans-serif; + padding-top: 30px; + padding-left: 30px; + padding-right: 30px; + padding-bottom: 20px; + display: table; + flex-direction: row; + background-color: #CCC; + border-bottom: 2px solid #888; + width: 100%; +} + +.banner2 > DIV { + display: table-row; +} + +.banner2 > DIV > DIV { + display: table-cell; + padding-bottom: 10px; +} + +.banner2_key { + min-width: 100px; + white-space: nowrap; + font-weight: bold; +} + +.banner2_value { + padding-left: 20px; + width: 100%; +} + +.banner3 { + font-family: sans-serif; + padding-top: 20px; + padding-left: 30px; + padding-right: 30px; + padding-bottom: 20px; + background-color: #EEE; + border-bottom: 2px solid #888; + width: 100%; +} + +.pageButtons { + display: flex; + flex-wrap: wrap; +} + +.pageButton { + background-color: #FFF; + color: #666; + padding: 10px; + margin: 5px; + border-radius: 8px; + text-decoration: none; + line-height: 0.5em; + border: 1px solid #FFF; +} + +.pageButton:HOVER { + border: 1px solid #888; +} + +.pageButtonSelected { + background-color: #888; + color: #FFF; +} + +.resourceCountBadge { + font-size: 0.8em; + background: #DDD; + padding: 4px; + border-radius: 6px; + color: #000; +} diff --git a/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.html b/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.html new file mode 100644 index 00000000000..029918433e3 --- /dev/null +++ b/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/index.html @@ -0,0 +1,66 @@ + + + + Swagger UI + + + + + + + +

    +
    + +
    +
    +
    FHIR Server Base URL
    +
    +
    +
    OpenAPI Docs
    +
    +
    +
    FHIR Version
    [[${FHIR_VERSION}]] ([[${FHIR_VERSION_CODENAME}]])
    +
    +
    +
    +
    + +
    + + + + + + diff --git a/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/raccoon.png b/hapi-fhir-server-openapi/src/main/resources/ca/uhn/fhir/rest/openapi/raccoon.png new file mode 100644 index 0000000000000000000000000000000000000000..cf0439606802ea70ff2e7c3bdd74e1beaa5d1184 GIT binary patch literal 83876 zcmZTvWl$Sk+fD+(-L<$wakt49{Q@NuKuFY?Ed$- zo)~`8m2h!w(`OLk(@>!CL&)qRF#P6pHII^tO7gt$^IId&t#D%J*3A&~nuFWwMCMug{EGR{F$C^W+!t*hw{9B`@9d|e?XgJK%+8APE0(1 z-?o=^S_t{ad6V0G1up&`__R&++76DK2Vu2`n-G~XCpqOfIv`SGxwEI->X#2kme3Lp zx6#jtymWtZ${f@e&fzY2ntFwQ&~ZK<{wi^K_ijyWtI0V04e^`D&S@FxZo*tYScLKm z6_fvB*ZbtfNrT*#U*)rlxSz_C#emUF;jAU@Aon%uRiyIlM8{BR^JQ$;zc!MO%iJ`1 zC*3qDt=i2h#k8U?=aG_6dhN~1jw)1Nx~{8To}qV+`L<|m?VnD67{fqaqSRvgbpAS= zn_Fp?dC;6EeU8QCktWWOS5W|=&rd;%yyetM#|NXKcuM?Y3jr1IlN>7xeF{F2j!W`$ zgnEt$|JBJd@4-KB>FDU_-rt>m$ik^kp*ki>?}$^66GjB6VgDLoLq>D*=p^t3%}MNf zK6ME)y<>kr9$Wcqb~aB%NvZr`rbr?5e9CPrhX@#A+2^H}cp~^c969_JBz@iKdo4o$ z)T~`|Hd8Zk_56fak&W{5tmQVZ6^yAR9A}HACAFoL#7Jq{@HIJaB8cg;{|3YFJ)?N1*tRp#5E+a$;4IwTbj_r@vmq+8<)73>I zP0g93Yu^Mxet!NpN%X6!D=S84?Pp!9=TDbYEhkI$MV<>)hSlZe<+xIh*1#C~Co&LO zx7|k9^9P^bK|@{b9M2x~-m2aOdFMS%h2kA_n9BHMW#@|D&ygSHJqXQEut<66x%}_W z2kR||1on1z_-w4L-%ts;nuVU*DE@UIc;l386V|2|zk!=ilADtg7y6weXM~=RUka^5 zm9;0GT+64-f2%+y66w?5HJ^@}ni>YYXbOCLLXo#~A6jr;-6VcZPvhWnMT@=MZGT}< zNo_35P-OVCnBOGCzA}*0KuN879%OMjo!ZrK$nMJ(W{sB6NefEQo5tWWtBKiz|A{JN zVW{{<@a6JyT&#JorXl%!_0RPANS`A?;=xvyZ*iC*?HwKqJG&#Lu=|Io2-Tia!!lea z8C9I_bo%zN$e^Bf3)A1J>e_{V+N{T#P$SmZnEsvo3+Y7@Kc~8e20SimAQChbS_F=Q z(!)m_tvVVa2!Mov6G8)J_kkD~=1lu@F`7$1-|CM8jQE2j zeWr}}YO@muw%9pj=d@)bUPo8A>5Z^EJ*|&S+qg59T&>fNB{yH z04hU^^}k>pZEcUIn)bhFS7#fN%r{J3=&E>}?@d8OGEV1u#t$(JKbA`36;Oi2SyuA_ z;oLT0da|F6C^aUXB6of0oP6jcjeG(E@QDzs71kB7brcZIZY`dPiK(UVeN?lrnu*Ep?t#4`j+_YDUgh8o*tQ}o9?UlRnXx=- zdO~pH#L3?NLcWkXe>4A4+5Qu=K{jjC%XMwgb+th~i2qBCuCJ1GpAd3*O-T!bfQ!@N zSk@1(vlqJZZJ$14mu*<`4}o}Pf0MShwqS9;o4cE{jSUK{NaJ`a$r@{p6+1=PpL8f9 zh->FH>b!sk7`I@P>$7(^SFUZmV>lG94~!-v6ZTnT(^GL$`6e1Y;oXIDR4H8+qNY

    uJP) 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Ð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 t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Patient.class))) + .withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Observation.class))) + .withServer(t -> t.registerProvider(new MyLastNProvider())) + .withServer(t -> t.registerInterceptor(new ResponseHighlighterInterceptor())); + private CloseableHttpClient myClient; + + @BeforeEach + public void before() { + myClient = HttpClientBuilder.create().build(); + } + + @AfterEach + public void after() throws IOException { + myClient.close(); + myServer.getRestfulServer().getInterceptorService().unregisterAllInterceptors(); + } + + @Test + public void testFetchSwagger() throws IOException { + myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); + + String resp; + HttpGet get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/metadata?_pretty=true"); + try (CloseableHttpResponse response = myClient.execute(get)) { + resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("CapabilityStatement: {}", resp); + } + + get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/api-docs"); + try (CloseableHttpResponse response = myClient.execute(get)) { + resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response: {}", response.getStatusLine()); + ourLog.info("Response: {}", resp); + } + + OpenAPI parsed = Yaml.mapper().readValue(resp, OpenAPI.class); + + PathItem fooOpPath = parsed.getPaths().get("/$foo-op"); + assertNull(fooOpPath.getGet()); + assertNotNull(fooOpPath.getPost()); + assertEquals("Foo Op Description", fooOpPath.getPost().getDescription()); + assertEquals("Foo Op Short", fooOpPath.getPost().getSummary()); + + PathItem lastNPath = parsed.getPaths().get("/Observation/$lastn"); + assertNull(lastNPath.getPost()); + assertNotNull(lastNPath.getGet()); + assertEquals("LastN Description", lastNPath.getGet().getDescription()); + assertEquals("LastN Short", lastNPath.getGet().getSummary()); + assertEquals(4, lastNPath.getGet().getParameters().size()); + assertEquals("Subject description", lastNPath.getGet().getParameters().get(0).getDescription()); + } + + @Test + public void testRedirectFromBaseUrl() throws IOException { + myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); + + HttpGet get; + + get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/"); + try (CloseableHttpResponse response = myClient.execute(get)) { + assertEquals(400, response.getStatusLine().getStatusCode()); + } + + get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/"); + get.addHeader(Constants.HEADER_ACCEPT, Constants.CT_HTML); + try (CloseableHttpResponse response = myClient.execute(get)) { + String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response: {}", response); + ourLog.info("Response: {}", responseString); + assertEquals(200, response.getStatusLine().getStatusCode()); + assertThat(responseString, containsString("Swagger UI")); + } + + } + + + @Test + public void testSwaggerUiWithResourceCounts() throws IOException { + myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); + myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); + + String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; + String resp = fetchSwaggerUi(url); + List buttonTexts = parsePageButtonTexts(resp, url); + assertThat(buttonTexts.toString(), buttonTexts, Matchers.contains("All", "System Level Operations", "Patient 2", "OperationDefinition 1", "Observation 0")); + } + + @Test + public void testSwaggerUiWithCopyright() throws IOException { + myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor()); + myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); + + String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; + String resp = fetchSwaggerUi(url); + assertThat(resp, resp, containsString("

    This server is copyright Example Org 2021

    ")); + } + + @Test + public void testSwaggerUiWithResourceCounts_OneResourceOnly() throws IOException { + myServer.getRestfulServer().registerInterceptor(new AddResourceCountsInterceptor("OperationDefinition")); + myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); + + String url = "http://localhost:" + myServer.getPort() + "/fhir/swagger-ui/"; + String resp = fetchSwaggerUi(url); + List buttonTexts = parsePageButtonTexts(resp, url); + assertThat(buttonTexts.toString(), buttonTexts, Matchers.contains("All", "System Level Operations", "OperationDefinition 1", "Observation", "Patient")); + } + + private String fetchSwaggerUi(String url) throws IOException { + String resp; + HttpGet get = new HttpGet(url); + try (CloseableHttpResponse response = myClient.execute(get)) { + resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response: {}", response.getStatusLine()); + ourLog.info("Response: {}", resp); + } + return resp; + } + + private List parsePageButtonTexts(String resp, String url) throws IOException { + HtmlPage html = HtmlUtil.parseAsHtml(resp, new URL(url)); + HtmlDivision pageButtons = (HtmlDivision) html.getElementById("pageButtons"); + List buttonTexts = new ArrayList<>(); + for (DomElement next : pageButtons.getChildElements()) { + buttonTexts.add(next.asNormalizedText()); + } + return buttonTexts; + } + + + public static class AddResourceCountsInterceptor { + + private final HashSet myResourceNamesToAddTo; + + public AddResourceCountsInterceptor(String... theResourceNamesToAddTo) { + myResourceNamesToAddTo = new HashSet<>(Arrays.asList(theResourceNamesToAddTo)); + } + + @Hook(Pointcut.SERVER_CAPABILITY_STATEMENT_GENERATED) + public void capabilityStatementGenerated(IBaseConformance theCapabilityStatement) { + CapabilityStatement cs = (CapabilityStatement) theCapabilityStatement; + cs.setCopyright("This server is copyright **Example Org** 2021"); + + int numResources = cs.getRestFirstRep().getResource().size(); + for (int i = 0; i < numResources; i++) { + + CapabilityStatement.CapabilityStatementRestResourceComponent restResource = cs.getRestFirstRep().getResource().get(i); + if (!myResourceNamesToAddTo.isEmpty() && !myResourceNamesToAddTo.contains(restResource.getType())) { + continue; + } + + restResource.addExtension( + ExtensionConstants.CONF_RESOURCE_COUNT, + new DecimalType(i) // reverse order + ); + + } + } + + } + + public static class MyLastNProvider { + + + @Description(value = "LastN Description", shortDefinition = "LastN Short") + @Operation(name = Constants.OPERATION_LASTN, typeName = "Observation", idempotent = true) + public IBaseBundle lastN( + @Description(value = "Subject description", shortDefinition = "Subject short", example = {"Patient/456", "Patient/789"}) + @OperationParam(name = "subject", typeName = "reference", min = 0, max = 1) IBaseReference theSubject, + @OperationParam(name = "category", typeName = "coding", min = 0, max = OperationParam.MAX_UNLIMITED) List theCategories, + @OperationParam(name = "code", typeName = "coding", min = 0, max = OperationParam.MAX_UNLIMITED) List theCodes, + @OperationParam(name = "max", typeName = "integer", min = 0, max = 1) IPrimitiveType theMax + ) { + throw new IllegalStateException(); + } + + @Description(value = "Foo Op Description", shortDefinition = "Foo Op Short") + @Operation(name = "foo-op", idempotent = false) + public IBaseBundle foo( + ServletRequestDetails theRequestDetails, + @Description(shortDefinition = "Reference description", example = "Patient/123") + @OperationParam(name = "subject", typeName = "reference", min = 0, max = 1) IBaseReference theSubject, + @OperationParam(name = "category", typeName = "coding", min = 0, max = OperationParam.MAX_UNLIMITED) List theCategories, + @OperationParam(name = "code", typeName = "coding", min = 0, max = OperationParam.MAX_UNLIMITED) List theCodes, + @OperationParam(name = "max", typeName = "integer", min = 0, max = 1) IPrimitiveType theMax + ) { + throw new IllegalStateException(); + } + + @Patch(type = Patient.class) + public MethodOutcome patch(HttpServletRequest theRequest, @IdParam IIdType theId, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails, @ResourceParam String theBody, PatchTypeEnum thePatchType, @ResourceParam IBaseParameters theRequestBody) { + throw new IllegalStateException(); + } + + + } +} diff --git a/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorWithAuthorizationInterceptorTest.java b/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorWithAuthorizationInterceptorTest.java new file mode 100644 index 00000000000..766e79e5d89 --- /dev/null +++ b/hapi-fhir-server-openapi/src/test/java/ca/uhn/fhir/rest/openapi/OpenApiInterceptorWithAuthorizationInterceptorTest.java @@ -0,0 +1,129 @@ +package ca.uhn.fhir.rest.openapi; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.interceptor.api.Hook; +import ca.uhn.fhir.interceptor.api.Pointcut; +import ca.uhn.fhir.model.api.annotation.Description; +import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.Patch; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.PatchTypeEnum; +import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; +import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor; +import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRule; +import ca.uhn.fhir.rest.server.interceptor.auth.RuleBuilder; +import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.test.utilities.HtmlUtil; +import ca.uhn.fhir.test.utilities.server.RestfulServerExtension; +import ca.uhn.fhir.util.ExtensionConstants; +import com.gargoylesoftware.htmlunit.html.DomElement; +import com.gargoylesoftware.htmlunit.html.HtmlDivision; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.PathItem; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.hamcrest.Matchers; +import org.hl7.fhir.instance.model.api.IBaseBundle; +import org.hl7.fhir.instance.model.api.IBaseCoding; +import org.hl7.fhir.instance.model.api.IBaseConformance; +import org.hl7.fhir.instance.model.api.IBaseParameters; +import org.hl7.fhir.instance.model.api.IBaseReference; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.r4.model.CapabilityStatement; +import org.hl7.fhir.r4.model.DecimalType; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Patient; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class OpenApiInterceptorWithAuthorizationInterceptorTest { + + private static final Logger ourLog = LoggerFactory.getLogger(OpenApiInterceptorWithAuthorizationInterceptorTest.class); + private FhirContext myFhirContext = FhirContext.forCached(FhirVersionEnum.R4); + @RegisterExtension + @Order(0) + protected RestfulServerExtension myServer = new RestfulServerExtension(myFhirContext) + .withServletPath("/fhir/*") + .withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Patient.class))) + .withServer(t -> t.registerProvider(new HashMapResourceProvider<>(myFhirContext, Observation.class))) + .withServer(t -> t.registerProvider(new OpenApiInterceptorTest.MyLastNProvider())) + .withServer(t -> t.registerInterceptor(new ResponseHighlighterInterceptor())); + private CloseableHttpClient myClient; + private AuthorizationInterceptor myAuthorizationInterceptor; + private List myRules; + + @BeforeEach + public void before() { + myClient = HttpClientBuilder.create().build(); + myAuthorizationInterceptor = new AuthorizationInterceptor() { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return myRules; + } + }; + } + + @AfterEach + public void after() throws IOException { + myClient.close(); + myServer.getRestfulServer().getInterceptorService().unregisterAllInterceptors(); + } + + @Test + public void testFetchSwagger_AllowAll() throws IOException { + myServer.getRestfulServer().registerInterceptor(new OpenApiInterceptor()); + myServer.getRestfulServer().registerInterceptor(myAuthorizationInterceptor); + + myRules = new RuleBuilder() + .allowAll() + .build(); + + String resp; + HttpGet get; + + get = new HttpGet("http://localhost:" + myServer.getPort() + "/fhir/api-docs"); + try (CloseableHttpResponse response = myClient.execute(get)) { + resp = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response: {}", response.getStatusLine()); + ourLog.info("Response: {}", resp); + assertEquals(200, response.getStatusLine().getStatusCode()); + } + + OpenAPI parsed = Yaml.mapper().readValue(resp, OpenAPI.class); + assertNotNull(parsed.getPaths().get("/Patient").getPost()); + } +} diff --git a/hapi-fhir-server-openapi/src/test/resources/logback-test.xml b/hapi-fhir-server-openapi/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..2e29c0dfe82 --- /dev/null +++ b/hapi-fhir-server-openapi/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + INFO + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] %msg%n + + + + + + + + diff --git a/hapi-fhir-server/pom.xml b/hapi-fhir-server/pom.xml index 4fe87a2ee16..b0334c6fbbc 100644 --- a/hapi-fhir-server/pom.xml +++ b/hapi-fhir-server/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java index 5f7a405a59c..a2e541fb5fc 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/api/server/RequestDetails.java @@ -7,6 +7,7 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.server.IRestfulServerDefaults; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.UrlUtil; import org.apache.commons.lang3.Validate; @@ -51,7 +52,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; public abstract class RequestDetails { - private final StopWatch myRequestStopwatch = new StopWatch(); + private final StopWatch myRequestStopwatch; private IInterceptorBroadcaster myInterceptorBroadcaster; private String myTenantId; private String myCompartmentName; @@ -81,6 +82,37 @@ public abstract class RequestDetails { */ public RequestDetails(IInterceptorBroadcaster theInterceptorBroadcaster) { myInterceptorBroadcaster = theInterceptorBroadcaster; + myRequestStopwatch = new StopWatch(); + } + + /** + * Copy constructor + */ + public RequestDetails(ServletRequestDetails theRequestDetails) { + myInterceptorBroadcaster = theRequestDetails.getInterceptorBroadcaster(); + myRequestStopwatch = theRequestDetails.getRequestStopwatch(); + myTenantId = theRequestDetails.getTenantId(); + myCompartmentName = theRequestDetails.getCompartmentName(); + myCompleteUrl = theRequestDetails.getCompleteUrl(); + myFhirServerBase = theRequestDetails.getFhirServerBase(); + myId = theRequestDetails.getId(); + myOperation = theRequestDetails.getOperation(); + myParameters = theRequestDetails.getParameters(); + myRequestContents = theRequestDetails.getRequestContentsIfLoaded(); + myRequestPath = theRequestDetails.getRequestPath(); + myRequestType = theRequestDetails.getRequestType(); + myResourceName = theRequestDetails.getResourceName(); + myRespondGzip = theRequestDetails.isRespondGzip(); + myResponse = theRequestDetails.getResponse(); + myRestOperationType = theRequestDetails.getRestOperationType(); + mySecondaryOperation = theRequestDetails.getSecondaryOperation(); + mySubRequest = theRequestDetails.isSubRequest(); + myUnqualifiedToQualifiedNames = theRequestDetails.getUnqualifiedToQualifiedNames(); + myUserData = theRequestDetails.getUserData(); + myResource = theRequestDetails.getResource(); + myRequestId = theRequestDetails.getRequestId(); + myTransactionGuid = theRequestDetails.getTransactionGuid(); + myFixedConditionalUrl = theRequestDetails.getFixedConditionalUrl(); } public String getFixedConditionalUrl() { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/Bindings.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/Bindings.java index a449d2bf88a..f17d1bf04c5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/Bindings.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/Bindings.java @@ -30,14 +30,14 @@ import java.util.List; public class Bindings { private final IdentityHashMap myNamedSearchMethodBindingToName; private final HashMap> mySearchNameToBindings; - private final HashMap> myOperationNameToBindings; - private final IdentityHashMap myOperationBindingToName; + private final HashMap> myOperationIdToBindings; + private final IdentityHashMap myOperationBindingToId; - public Bindings(IdentityHashMap theNamedSearchMethodBindingToName, HashMap> theSearchNameToBindings, HashMap> theOperationNameToBindings, IdentityHashMap theOperationBindingToName) { + public Bindings(IdentityHashMap theNamedSearchMethodBindingToName, HashMap> theSearchNameToBindings, HashMap> theOperationIdToBindings, IdentityHashMap theOperationBindingToName) { myNamedSearchMethodBindingToName = theNamedSearchMethodBindingToName; mySearchNameToBindings = theSearchNameToBindings; - myOperationNameToBindings = theOperationNameToBindings; - myOperationBindingToName = theOperationBindingToName; + myOperationIdToBindings = theOperationIdToBindings; + myOperationBindingToId = theOperationBindingToName; } public IdentityHashMap getNamedSearchMethodBindingToName() { @@ -48,11 +48,11 @@ public class Bindings { return mySearchNameToBindings; } - public HashMap> getOperationNameToBindings() { - return myOperationNameToBindings; + public HashMap> getOperationIdToBindings() { + return myOperationIdToBindings; } - public IdentityHashMap getOperationBindingToName() { - return myOperationBindingToName; + public IdentityHashMap getOperationBindingToId() { + return myOperationBindingToId; } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/IServerConformanceProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/IServerConformanceProvider.java index 1e7459c9068..854f625e268 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/IServerConformanceProvider.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/IServerConformanceProvider.java @@ -22,8 +22,11 @@ package ca.uhn.fhir.rest.server; import javax.servlet.http.HttpServletRequest; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Read; import ca.uhn.fhir.rest.api.server.RequestDetails; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; public interface IServerConformanceProvider { @@ -34,6 +37,11 @@ public interface IServerConformanceProvider { */ T getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails); + @Read(typeName = "OperationDefinition") + default IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) { + return null; + } + /** * This setter is needed in implementation classes (along with * a no-arg constructor) to avoid reference cycles in the diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index 51439570c7f..60385fd101a 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -67,6 +67,7 @@ import com.google.common.collect.Lists; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; +import org.hl7.fhir.instance.model.api.IBaseConformance; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.slf4j.Logger; @@ -148,6 +149,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer myServerConformanceMethod; + private ConformanceMethodBinding myServerConformanceMethod; private Object myServerConformanceProvider; private String myServerName = "HAPI FHIR Server"; /** @@ -234,6 +236,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer> getGlobalBindings() { + return myGlobalBinding.getMethodBindings(); + } + protected List createPoweredByAttributes() { return Lists.newArrayList("FHIR Server", "FHIR " + myFhirContext.getVersion().getVersion().getFhirVersionString() + "/" + myFhirContext.getVersion().getVersion().name()); } @@ -458,7 +465,10 @@ public class RestfulServer extends HttpServlet implements IRestfulServer getResourceProviders() { - return myResourceProviders; + public List getResourceProviders() { + return Collections.unmodifiableList(myResourceProviders); } /** @@ -853,6 +877,7 @@ public class RestfulServer extends HttpServlet implements IRestfulServer resourceBindings; private List> serverBindings; + private List> myGlobalBindings; private Map> resourceNameToSharedSupertype; - private String implementationDescription; - private String serverVersion = VersionUtil.getVersion(); - private String serverName = "HAPI FHIR"; - private FhirContext fhirContext; - private IServerAddressStrategy serverAddressStrategy; + private String myImplementationDescription; + private String myServerName = "HAPI FHIR"; + private String myServerVersion = VersionUtil.getVersion(); + private FhirContext myFhirContext; + private IServerAddressStrategy myServerAddressStrategy; private IPrimitiveType myConformanceDate; /** @@ -127,10 +135,10 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { * @return the implementationDescription */ public String getImplementationDescription() { - if (isBlank(implementationDescription)) { + if (isBlank(myImplementationDescription)) { return "HAPI FHIR"; } - return implementationDescription; + return myImplementationDescription; } /** @@ -139,7 +147,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { * @param implementationDescription the implementationDescription to set */ public RestfulServerConfiguration setImplementationDescription(String implementationDescription) { - this.implementationDescription = implementationDescription; + this.myImplementationDescription = implementationDescription; return this; } @@ -149,7 +157,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { * @return the serverVersion */ public String getServerVersion() { - return serverVersion; + return myServerVersion; } /** @@ -158,7 +166,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { * @param serverVersion the serverVersion to set */ public RestfulServerConfiguration setServerVersion(String serverVersion) { - this.serverVersion = serverVersion; + this.myServerVersion = serverVersion; return this; } @@ -168,7 +176,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { * @return the serverName */ public String getServerName() { - return serverName; + return myServerName; } /** @@ -177,7 +185,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { * @param serverName the serverName to set */ public RestfulServerConfiguration setServerName(String serverName) { - this.serverName = serverName; + this.myServerName = serverName; return this; } @@ -186,7 +194,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { * creating their own. */ public FhirContext getFhirContext() { - return this.fhirContext; + return this.myFhirContext; } /** @@ -195,7 +203,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { * @param fhirContext the fhirContext to set */ public RestfulServerConfiguration setFhirContext(FhirContext fhirContext) { - this.fhirContext = fhirContext; + this.myFhirContext = fhirContext; return this; } @@ -205,7 +213,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { * @return the serverAddressStrategy */ public IServerAddressStrategy getServerAddressStrategy() { - return serverAddressStrategy; + return myServerAddressStrategy; } /** @@ -214,7 +222,7 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { * @param serverAddressStrategy the serverAddressStrategy to set */ public void setServerAddressStrategy(IServerAddressStrategy serverAddressStrategy) { - this.serverAddressStrategy = serverAddressStrategy; + this.myServerAddressStrategy = serverAddressStrategy; } /** @@ -236,48 +244,114 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { } public Bindings provideBindings() { - IdentityHashMap myNamedSearchMethodBindingToName = new IdentityHashMap<>(); - HashMap> mySearchNameToBindings = new HashMap<>(); - IdentityHashMap myOperationBindingToName = new IdentityHashMap<>(); - HashMap> myOperationNameToBindings = new HashMap<>(); + IdentityHashMap namedSearchMethodBindingToName = new IdentityHashMap<>(); + HashMap> searchNameToBindings = new HashMap<>(); + IdentityHashMap operationBindingToId = new IdentityHashMap<>(); + HashMap> operationIdToBindings = new HashMap<>(); Map>> resourceToMethods = collectMethodBindings(); - for (Map.Entry>> nextEntry : resourceToMethods.entrySet()) { - List> nextMethodBindings = nextEntry.getValue(); - for (BaseMethodBinding nextMethodBinding : nextMethodBindings) { - if (nextMethodBinding instanceof OperationMethodBinding) { - OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - if (myOperationBindingToName.containsKey(methodBinding)) { - continue; - } + List> methodBindings = resourceToMethods + .values() + .stream().flatMap(t -> t.stream()) + .collect(Collectors.toList()); + if (myGlobalBindings != null) { + methodBindings.addAll(myGlobalBindings); + } - String name = createOperationName(methodBinding); - ourLog.debug("Detected operation: {}", name); - - myOperationBindingToName.put(methodBinding, name); - if (myOperationNameToBindings.containsKey(name) == false) { - myOperationNameToBindings.put(name, new ArrayList<>()); - } - myOperationNameToBindings.get(name).add(methodBinding); - } else if (nextMethodBinding instanceof SearchMethodBinding) { - SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding; - if (myNamedSearchMethodBindingToName.containsKey(methodBinding)) { - continue; - } - - String name = createNamedQueryName(methodBinding); - ourLog.debug("Detected named query: {}", name); - - myNamedSearchMethodBindingToName.put(methodBinding, name); - if (!mySearchNameToBindings.containsKey(name)) { - mySearchNameToBindings.put(name, new ArrayList<>()); - } - mySearchNameToBindings.get(name).add(methodBinding); + ListMultimap nameToOperationMethodBindings = ArrayListMultimap.create(); + for (BaseMethodBinding nextMethodBinding : methodBindings) { + if (nextMethodBinding instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; + nameToOperationMethodBindings.put(methodBinding.getName(), methodBinding); + } else if (nextMethodBinding instanceof SearchMethodBinding) { + SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding; + if (namedSearchMethodBindingToName.containsKey(methodBinding)) { + continue; } + + String name = createNamedQueryName(methodBinding); + ourLog.debug("Detected named query: {}", name); + + namedSearchMethodBindingToName.put(methodBinding, name); + if (!searchNameToBindings.containsKey(name)) { + searchNameToBindings.put(name, new ArrayList<>()); + } + searchNameToBindings.get(name).add(methodBinding); } } - return new Bindings(myNamedSearchMethodBindingToName, mySearchNameToBindings, myOperationNameToBindings, myOperationBindingToName); + for (String nextName : nameToOperationMethodBindings.keySet()) { + List nextMethodBindings = nameToOperationMethodBindings.get(nextName); + + boolean global = false; + boolean system = false; + boolean instance = false; + boolean type = false; + Set resourceTypes = null; + + for (OperationMethodBinding nextMethodBinding : nextMethodBindings) { + global |= nextMethodBinding.isGlobalMethod(); + system |= nextMethodBinding.isCanOperateAtServerLevel(); + type |= nextMethodBinding.isCanOperateAtTypeLevel(); + instance |= nextMethodBinding.isCanOperateAtInstanceLevel(); + if (nextMethodBinding.getResourceName() != null) { + resourceTypes = resourceTypes != null ? resourceTypes : new TreeSet<>(); + resourceTypes.add(nextMethodBinding.getResourceName()); + } + } + + StringBuilder operationIdBuilder = new StringBuilder(); + if (global) { + operationIdBuilder.append("Global"); + } else if (resourceTypes != null && resourceTypes.size() == 1) { + operationIdBuilder.append(resourceTypes.iterator().next()); + } else if (resourceTypes != null && resourceTypes.size() == 2) { + Iterator iterator = resourceTypes.iterator(); + operationIdBuilder.append(iterator.next()); + operationIdBuilder.append(iterator.next()); + } else if (resourceTypes != null) { + operationIdBuilder.append("Multi"); + } + + operationIdBuilder.append('-'); + if (instance) { + operationIdBuilder.append('i'); + } + if (type) { + operationIdBuilder.append('t'); + } + if (system) { + operationIdBuilder.append('s'); + } + operationIdBuilder.append('-'); + + // Exclude the leading $ + operationIdBuilder.append(nextName, 1, nextName.length()); + + String operationId = operationIdBuilder.toString(); + operationIdToBindings.put(operationId, nextMethodBindings); + nextMethodBindings.forEach(t->operationBindingToId.put(t, operationId)); + } + + for (BaseMethodBinding nextMethodBinding : methodBindings) { + if (nextMethodBinding instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; + if (operationBindingToId.containsKey(methodBinding)) { + continue; + } + + String name = createOperationName(methodBinding); + ourLog.debug("Detected operation: {}", name); + + operationBindingToId.put(methodBinding, name); + if (operationIdToBindings.containsKey(name) == false) { + operationIdToBindings.put(name, new ArrayList<>()); + } + operationIdToBindings.get(name).add(methodBinding); + } + } + + return new Bindings(namedSearchMethodBindingToName, searchNameToBindings, operationIdToBindings, operationBindingToId); } public Map>> collectMethodBindings() { @@ -301,6 +375,14 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { return resourceToMethods; } + public List> getGlobalBindings() { + return myGlobalBindings; + } + + public void setGlobalBindings(List> theGlobalBindings) { + myGlobalBindings = theGlobalBindings; + } + /* * Populates {@link #resourceNameToSharedSupertype} by scanning the given resource providers. Only resource provider getResourceType values * are taken into account. {@link ProvidesResources} and method return types are deliberately ignored. @@ -329,28 +411,6 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { entry -> entry.getValue().getLowestCommonSuperclass().get())); } - private String createOperationName(OperationMethodBinding theMethodBinding) { - StringBuilder retVal = new StringBuilder(); - if (theMethodBinding.getResourceName() != null) { - retVal.append(theMethodBinding.getResourceName()); - } - - retVal.append('-'); - if (theMethodBinding.isCanOperateAtInstanceLevel()) { - retVal.append('i'); - } - if (theMethodBinding.isCanOperateAtServerLevel()) { - retVal.append('s'); - } - retVal.append('-'); - - // Exclude the leading $ - retVal.append(theMethodBinding.getName(), 1, theMethodBinding.getName().length()); - - return retVal.toString(); - } - - private String createNamedQueryName(SearchMethodBinding searchMethodBinding) { StringBuilder retVal = new StringBuilder(); if (searchMethodBinding.getResourceName() != null) { @@ -443,7 +503,6 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { } - private static class SearchParameterComparator implements Comparator { private static final SearchParameterComparator INSTANCE = new SearchParameterComparator(); @@ -458,4 +517,27 @@ public class RestfulServerConfiguration implements ISearchParamRegistry { return 1; } } + + private static String createOperationName(OperationMethodBinding theMethodBinding) { + StringBuilder retVal = new StringBuilder(); + if (theMethodBinding.getResourceName() != null) { + retVal.append(theMethodBinding.getResourceName()); + } else if (theMethodBinding.isGlobalMethod()) { + retVal.append("Global"); + } + + retVal.append('-'); + if (theMethodBinding.isCanOperateAtInstanceLevel()) { + retVal.append('i'); + } + if (theMethodBinding.isCanOperateAtServerLevel()) { + retVal.append('s'); + } + retVal.append('-'); + + // Exclude the leading $ + retVal.append(theMethodBinding.getName(), 1, theMethodBinding.getName().length()); + + return retVal.toString(); + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java index 8b83bc55a56..229a7e49802 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseMethodBinding.java @@ -41,6 +41,7 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetai import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ReflectionUtil; import org.hl7.fhir.instance.model.api.IAnyResource; +import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; import javax.annotation.Nonnull; @@ -386,6 +387,10 @@ public abstract class BaseMethodBinding { + " returns a collection with generic type " + toLogString(returnTypeFromMethod) + " - Must return a resource type or a collection (List, Set) with a resource type parameter (e.g. List or List )"); } + } else if (IBaseBundle.class.isAssignableFrom(returnTypeFromMethod) && returnTypeFromRp == null) { + // If a plain provider method returns a Bundle, we'll assume it to be a system + // level operation and not a type/instance level operation on the Bundle type. + returnTypeFromMethod = null; } else { if (!isResourceInterface(returnTypeFromMethod) && !verifyIsValidResourceReturnType(returnTypeFromMethod)) { throw new ConfigurationException("Method '" + theMethod.getName() + "' from " + IResourceProvider.class.getSimpleName() + " type " + theMethod.getDeclaringClass().getCanonicalName() @@ -395,30 +400,41 @@ public abstract class BaseMethodBinding { } Class returnTypeFromAnnotation = IBaseResource.class; + String returnTypeNameFromAnnotation = null; if (read != null) { - if (isNotBlank(read.typeName())) { - returnTypeFromAnnotation = theContext.getResourceDefinition(read.typeName()).getImplementingClass(); - } else { - returnTypeFromAnnotation = read.type(); - } + returnTypeFromAnnotation = read.type(); + returnTypeNameFromAnnotation = read.typeName(); } else if (search != null) { returnTypeFromAnnotation = search.type(); + returnTypeNameFromAnnotation = search.typeName(); } else if (history != null) { returnTypeFromAnnotation = history.type(); + returnTypeNameFromAnnotation = history.typeName(); } else if (delete != null) { returnTypeFromAnnotation = delete.type(); + returnTypeNameFromAnnotation = delete.typeName(); } else if (patch != null) { returnTypeFromAnnotation = patch.type(); + returnTypeNameFromAnnotation = patch.typeName(); } else if (create != null) { returnTypeFromAnnotation = create.type(); + returnTypeNameFromAnnotation = create.typeName(); } else if (update != null) { returnTypeFromAnnotation = update.type(); + returnTypeNameFromAnnotation = update.typeName(); } else if (validate != null) { returnTypeFromAnnotation = validate.type(); + returnTypeNameFromAnnotation = validate.typeName(); } else if (addTags != null) { returnTypeFromAnnotation = addTags.type(); + returnTypeNameFromAnnotation = addTags.typeName(); } else if (deleteTags != null) { returnTypeFromAnnotation = deleteTags.type(); + returnTypeNameFromAnnotation = deleteTags.typeName(); + } + + if (isNotBlank(returnTypeNameFromAnnotation)) { + returnTypeFromAnnotation = theContext.getResourceDefinition(returnTypeNameFromAnnotation).getImplementingClass(); } if (returnTypeFromRp != null) { @@ -477,7 +493,7 @@ public abstract class BaseMethodBinding { } private static boolean isResourceInterface(Class theReturnTypeFromMethod) { - return theReturnTypeFromMethod.equals(IBaseResource.class) || theReturnTypeFromMethod.equals(IResource.class) || theReturnTypeFromMethod.equals(IAnyResource.class); + return theReturnTypeFromMethod != null && (theReturnTypeFromMethod.equals(IBaseResource.class) || theReturnTypeFromMethod.equals(IResource.class) || theReturnTypeFromMethod.equals(IAnyResource.class)); } private static String toLogString(Class theType) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java index 5c37205e17b..302eabac9ea 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/BaseResourceReturningMethodBinding.java @@ -27,6 +27,8 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.ReflectionUtil; +import ca.uhn.fhir.util.ValidateUtil; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -79,7 +81,13 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi super(theMethod, theContext, theProvider); Class methodReturnType = theMethod.getReturnType(); - if (Collection.class.isAssignableFrom(methodReturnType)) { + + Set> expectedReturnTypes = provideExpectedReturnTypes(); + if (expectedReturnTypes != null) { + + Validate.isTrue(expectedReturnTypes.contains(methodReturnType), "Unexpected method return type on %s - Allowed: %s", theMethod, expectedReturnTypes); + + } else if (Collection.class.isAssignableFrom(methodReturnType)) { myMethodReturnType = MethodReturnTypeEnum.LIST_OF_RESOURCES; Class collectionType = ReflectionUtil.getGenericCollectionTypeOfMethodReturnType(theMethod); @@ -123,6 +131,13 @@ public abstract class BaseResourceReturningMethodBinding extends BaseMethodBindi } + /** + * Subclasses may override + */ + protected Set> provideExpectedReturnTypes() { + return null; + } + IBaseResource createBundleFromBundleProvider(IRestfulServer theServer, RequestDetails theRequest, Integer theLimit, String theLinkSelf, Set theIncludes, IBundleProvider theResult, int theOffset, BundleTypeEnum theBundleType, EncodingEnum theLinkEncoding, String theSearchId) { IVersionSpecificBundleFactory bundleFactory = theServer.getFhirContext().newBundleFactory(); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ConformanceMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ConformanceMethodBinding.java index 02db0a82632..1930d176f5b 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ConformanceMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ConformanceMethodBinding.java @@ -33,6 +33,7 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IRestfulServer; import ca.uhn.fhir.rest.api.server.RequestDetails; +import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.SimpleBundleProvider; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; @@ -194,6 +195,7 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding myCachedResponse.set(conf); myCachedResponseExpires.set(System.currentTimeMillis() + getCacheMillis()); } + return conf; } @@ -230,4 +232,13 @@ public class ConformanceMethodBinding extends BaseResourceReturningMethodBinding return null; } + /** + * Create and return the server's CapabilityStatement + */ + public IBaseConformance provideCapabilityStatement(RestfulServer theServer, RequestDetails theRequest) { + Object[] params = createMethodParams(theRequest); + IBundleProvider resultObj = invokeServer(theServer, theRequest, params); + return (IBaseConformance) resultObj.getResources(0,1).get(0); + } + } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java index 96401f0dd04..35779595f40 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/GraphQLMethodBinding.java @@ -42,8 +42,10 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.Writer; import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Set; -public class GraphQLMethodBinding extends BaseMethodBinding { +public class GraphQLMethodBinding extends OperationMethodBinding { private final Integer myIdParamIndex; private final Integer myQueryUrlParamIndex; @@ -51,7 +53,7 @@ public class GraphQLMethodBinding extends BaseMethodBinding { private final RequestTypeEnum myMethodRequestType; public GraphQLMethodBinding(Method theMethod, RequestTypeEnum theMethodRequestType, FhirContext theContext, Object theProvider) { - super(theMethod, theContext, theProvider); + super(null, null, theMethod, theContext, theProvider, true, Constants.OPERATION_NAME_GRAPHQL, null, null, null, null, true); myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, theContext); myQueryUrlParamIndex = ParameterUtil.findParamAnnotationIndex(theMethod, GraphQLQueryUrl.class); @@ -71,10 +73,30 @@ public class GraphQLMethodBinding extends BaseMethodBinding { } @Override - public boolean isGlobalMethod() { + public RestOperationTypeEnum getRestOperationType(RequestDetails theRequestDetails) { + return getRestOperationType(); + } + + @Override + protected Set> provideExpectedReturnTypes() { + return Collections.singleton(String.class); + } + + @Override + public boolean isCanOperateAtServerLevel() { return true; } + @Override + public boolean isCanOperateAtTypeLevel() { + return false; + } + + @Override + public boolean isCanOperateAtInstanceLevel() { + return myIdParamIndex != null; + } + @Override public MethodMatchEnum incomingServerRequestMatchesMethod(RequestDetails theRequest) { if (Constants.OPERATION_NAME_GRAPHQL.equals(theRequest.getOperation()) && myMethodRequestType.equals(theRequest.getRequestType())) { diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java index e01d1daa859..31a608c2846 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/MethodUtil.java @@ -52,15 +52,19 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; public class MethodUtil { + /** + * Non instantiable + */ + private MethodUtil() { + // nothing + } + public static void extractDescription(SearchParameter theParameter, Annotation[] theAnnotations) { for (Annotation annotation : theAnnotations) { if (annotation instanceof Description) { Description desc = (Description) annotation; - if (isNotBlank(desc.formalDefinition())) { - theParameter.setDescription(desc.formalDefinition()); - } else { - theParameter.setDescription(desc.shortDefinition()); - } + String description = ParametersUtil.extractDescription(desc); + theParameter.setDescription(description); } } } @@ -72,7 +76,7 @@ public class MethodUtil { Class[] parameterTypes = theMethod.getParameterTypes(); int paramIndex = 0; - for (Annotation[] annotations : theMethod.getParameterAnnotations()) { + for (Annotation[] nextParameterAnnotations : theMethod.getParameterAnnotations()) { IParameter param = null; Class declaredParameterType = parameterTypes[paramIndex]; @@ -136,8 +140,8 @@ public class MethodUtil { } else if (parameterType.equals(SearchTotalModeEnum.class)) { param = new SearchTotalModeParameter(); } else { - for (int i = 0; i < annotations.length && param == null; i++) { - Annotation nextAnnotation = annotations[i]; + for (int i = 0; i < nextParameterAnnotations.length && param == null; i++) { + Annotation nextAnnotation = nextParameterAnnotations[i]; if (nextAnnotation instanceof RequiredParam) { SearchParameter parameter = new SearchParameter(); @@ -147,7 +151,7 @@ public class MethodUtil { parameter.setCompositeTypes(((RequiredParam) nextAnnotation).compositeTypes()); parameter.setChainLists(((RequiredParam) nextAnnotation).chainWhitelist(), ((RequiredParam) nextAnnotation).chainBlacklist()); parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); - MethodUtil.extractDescription(parameter, annotations); + MethodUtil.extractDescription(parameter, nextParameterAnnotations); param = parameter; } else if (nextAnnotation instanceof OptionalParam) { SearchParameter parameter = new SearchParameter(); @@ -157,7 +161,7 @@ public class MethodUtil { parameter.setCompositeTypes(((OptionalParam) nextAnnotation).compositeTypes()); parameter.setChainLists(((OptionalParam) nextAnnotation).chainWhitelist(), ((OptionalParam) nextAnnotation).chainBlacklist()); parameter.setType(theContext, parameterType, innerCollectionType, outerCollectionType); - MethodUtil.extractDescription(parameter, annotations); + MethodUtil.extractDescription(parameter, nextParameterAnnotations); param = parameter; } else if (nextAnnotation instanceof RawParam) { param = new RawParamsParameter(parameters); @@ -235,7 +239,9 @@ public class MethodUtil { } OperationParam operationParam = (OperationParam) nextAnnotation; - param = new OperationParameter(theContext, op.name(), operationParam); + String description = ParametersUtil.extractDescription(nextParameterAnnotations); + List examples = ParametersUtil.extractExamples(nextParameterAnnotations);; + param = new OperationParameter(theContext, op.name(), operationParam.name(), operationParam.min(), operationParam.max(), description, examples); if (isNotBlank(operationParam.typeName())) { BaseRuntimeElementDefinition elementDefinition = theContext.getElementDefinition(operationParam.typeName()); if (elementDefinition == null) { @@ -254,7 +260,9 @@ public class MethodUtil { throw new ConfigurationException( "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Mode.class.getSimpleName() + " must be of type " + ValidationModeEnum.class.getName()); } - param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1).setConverter(new IOperationParamConverter() { + String description = ParametersUtil.extractDescription(nextParameterAnnotations); + List examples = ParametersUtil.extractExamples(nextParameterAnnotations); + param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1, description, examples).setConverter(new IOperationParamConverter() { @Override public Object incomingServer(Object theObject) { if (isNotBlank(theObject.toString())) { @@ -277,7 +285,9 @@ public class MethodUtil { throw new ConfigurationException( "Parameter annotated with @" + Validate.class.getSimpleName() + "." + Validate.Profile.class.getSimpleName() + " must be of type " + String.class.getName()); } - param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, 0, 1).setConverter(new IOperationParamConverter() { + String description = ParametersUtil.extractDescription(nextParameterAnnotations); + List examples = ParametersUtil.extractExamples(nextParameterAnnotations); + param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_PROFILE, 0, 1, description, examples).setConverter(new IOperationParamConverter() { @Override public Object incomingServer(Object theObject) { return theObject.toString(); @@ -299,7 +309,7 @@ public class MethodUtil { if (param == null) { throw new ConfigurationException( "Parameter #" + ((paramIndex + 1)) + "/" + (parameterTypes.length) + " of method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() - + "' has no recognized FHIR interface parameter annotations. Don't know how to handle this parameter"); + + "' has no recognized FHIR interface parameter nextParameterAnnotations. Don't know how to handle this parameter"); } param.initializeTypes(theMethod, outerCollectionType, innerCollectionType, parameterType); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java index 5af53c33af2..b3dc81fe3f5 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationMethodBinding.java @@ -22,7 +22,6 @@ package ca.uhn.fhir.rest.server.method; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.valueset.BundleTypeEnum; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.annotation.IdParam; @@ -39,6 +38,7 @@ import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; +import ca.uhn.fhir.util.ParametersUtil; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.hl7.fhir.instance.model.api.IBase; @@ -64,6 +64,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { private final String myName; private final RestOperationTypeEnum myOtherOperationType; private final ReturnTypeEnum myReturnType; + private final String myShortDescription; private boolean myGlobal; private BundleTypeEnum myBundleType; private boolean myCanOperateAtInstanceLevel; @@ -74,24 +75,29 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { private boolean myManualRequestMode; private boolean myManualResponseMode; + /** + * Constructor - This is the constructor that is called when binding a + * standard @Operation method. + */ + public OperationMethodBinding(Class theReturnResourceType, Class theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, + Operation theAnnotation) { + this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.typeName(), theAnnotation.returnParameters(), + theAnnotation.bundleType(), theAnnotation.global()); + + myManualRequestMode = theAnnotation.manualRequest(); + myManualResponseMode = theAnnotation.manualResponse(); + } + protected OperationMethodBinding(Class theReturnResourceType, Class theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, boolean theIdempotent, String theOperationName, Class theOperationType, String theOperationTypeName, - OperationParam[] theReturnParams, BundleTypeEnum theBundleType) { + OperationParam[] theReturnParams, BundleTypeEnum theBundleType, boolean theGlobal) { super(theReturnResourceType, theMethod, theContext, theProvider); myBundleType = theBundleType; myIdempotent = theIdempotent; - - Description description = theMethod.getAnnotation(Description.class); - if (description != null) { - myDescription = description.formalDefinition(); - if (isBlank(myDescription)) { - myDescription = description.shortDefinition(); - } - } - if (isBlank(myDescription)) { - myDescription = null; - } + myDescription = ParametersUtil.extractDescription(theMethod); + myShortDescription = ParametersUtil.extractShortDefinition(theMethod); + myGlobal = theGlobal; for (Annotation[] nextParamAnnotations : theMethod.getParameterAnnotations()) { for (Annotation nextParam : nextParamAnnotations) { @@ -113,7 +119,7 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { try { if (theReturnTypeFromRp != null) { setResourceName(theContext.getResourceType(theReturnTypeFromRp)); - } else if (Modifier.isAbstract(theOperationType.getModifiers()) == false) { + } else if (theOperationType != null && Modifier.isAbstract(theOperationType.getModifiers()) == false) { setResourceName(theContext.getResourceType(theOperationType)); } else if (isNotBlank(theOperationTypeName)) { setResourceName(theContext.getResourceType(theOperationTypeName)); @@ -133,9 +139,10 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); if (getResourceName() == null) { myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_SERVER; - myCanOperateAtServerLevel = true; if (myIdParamIndex != null) { myCanOperateAtInstanceLevel = true; + } else { + myCanOperateAtServerLevel = true; } } else if (myIdParamIndex == null) { myOtherOperationType = RestOperationTypeEnum.EXTENDED_OPERATION_TYPE; @@ -169,20 +176,16 @@ public class OperationMethodBinding extends BaseResourceReturningMethodBinding { myReturnParams.add(type); } } + + // Parameter Validation + if (myCanOperateAtInstanceLevel && !isGlobalMethod() && getResourceName() == null) { + throw new ConfigurationException("@" + Operation.class.getSimpleName() + " method is an instance level method (it has an @" + IdParam.class.getSimpleName() + " parameter) but is not marked as global() and is not declared in a resource provider: " + theMethod.getName()); + } + } - /** - * Constructor - This is the constructor that is called when binding a - * standard @Operation method. - */ - public OperationMethodBinding(Class theReturnResourceType, Class theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, - Operation theAnnotation) { - this(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, theAnnotation.idempotent(), theAnnotation.name(), theAnnotation.type(), theAnnotation.typeName(), theAnnotation.returnParameters(), - theAnnotation.bundleType()); - - myManualRequestMode = theAnnotation.manualRequest(); - myManualResponseMode = theAnnotation.manualResponse(); - myGlobal = theAnnotation.global(); + public String getShortDescription() { + return myShortDescription; } @Override diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java index fc79dfdb0c5..13a9d115b2d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/OperationParameter.java @@ -20,6 +20,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.MethodNotAllowedException; import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.ParametersUtil; import ca.uhn.fhir.util.ReflectionUtil; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.*; @@ -68,17 +69,23 @@ public class OperationParameter implements IParameter { private Class myParameterType; private String myParamType; private SearchParameter mySearchParameterBinding; + private String myDescription; + private List myExampleValues; - public OperationParameter(FhirContext theCtx, String theOperationName, OperationParam theOperationParam) { - this(theCtx, theOperationName, theOperationParam.name(), theOperationParam.min(), theOperationParam.max()); - } - - OperationParameter(FhirContext theCtx, String theOperationName, String theParameterName, int theMin, int theMax) { + OperationParameter(FhirContext theCtx, String theOperationName, String theParameterName, int theMin, int theMax, String theDescription, List theExampleValues) { myOperationName = theOperationName; myName = theParameterName; myMin = theMin; myMax = theMax; myContext = theCtx; + myDescription = theDescription; + + List exampleValues = new ArrayList<>(); + if (theExampleValues != null) { + exampleValues.addAll(theExampleValues); + } + myExampleValues = Collections.unmodifiableList(exampleValues); + } @SuppressWarnings({"rawtypes", "unchecked"}) @@ -438,6 +445,14 @@ public class OperationParameter implements IParameter { } } + public String getDescription() { + return myDescription; + } + + public List getExampleValues() { + return myExampleValues; + } + interface IOperationParamConverter { Object incomingServer(Object theObject); diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java index cdb569d43e4..233414fcabc 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/SearchMethodBinding.java @@ -35,6 +35,7 @@ import ca.uhn.fhir.rest.param.ParameterUtil; import ca.uhn.fhir.rest.param.QualifierDetails; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.util.ParametersUtil; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -80,15 +81,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding { this.myCompartmentName = StringUtils.defaultIfBlank(search.compartmentName(), null); this.myIdParamIndex = ParameterUtil.findIdParameterIndex(theMethod, getContext()); this.myAllowUnknownParams = search.allowUnknownParams(); - - Description desc = theMethod.getAnnotation(Description.class); - if (desc != null) { - if (isNotBlank(desc.formalDefinition())) { - myDescription = StringUtils.defaultIfBlank(desc.formalDefinition(), null); - } else { - myDescription = StringUtils.defaultIfBlank(desc.shortDefinition(), null); - } - } + this.myDescription = ParametersUtil.extractDescription(theMethod); /* * Only compartment searching methods may have an ID parameter diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java index ee7cf623818..3568ac9e345 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/method/ValidateMethodBindingDstu2Plus.java @@ -20,10 +20,12 @@ package ca.uhn.fhir.rest.server.method; * #L% */ +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import ca.uhn.fhir.util.ParametersUtil; import org.hl7.fhir.instance.model.api.IBaseResource; import ca.uhn.fhir.context.FhirContext; @@ -37,9 +39,9 @@ public class ValidateMethodBindingDstu2Plus extends OperationMethodBinding { public ValidateMethodBindingDstu2Plus(Class theReturnResourceType, Class theReturnTypeFromRp, Method theMethod, FhirContext theContext, Object theProvider, Validate theAnnotation) { - super(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, true, Constants.EXTOP_VALIDATE, theAnnotation.type(), null, new OperationParam[0], BundleTypeEnum.COLLECTION); + super(theReturnResourceType, theReturnTypeFromRp, theMethod, theContext, theProvider, true, Constants.EXTOP_VALIDATE, theAnnotation.type(), null, new OperationParam[0], BundleTypeEnum.COLLECTION, false); - List newParams = new ArrayList(); + List newParams = new ArrayList<>(); int idx = 0; for (IParameter next : getParameters()) { if (next instanceof ResourceParameter) { @@ -48,7 +50,10 @@ public class ValidateMethodBindingDstu2Plus extends OperationMethodBinding { if (String.class.equals(parameterType) || EncodingEnum.class.equals(parameterType)) { newParams.add(next); } else { - OperationParameter parameter = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_RESOURCE, 0, 1); + Annotation[] parameterAnnotations = theMethod.getParameterAnnotations()[idx]; + String description = ParametersUtil.extractDescription(parameterAnnotations); + List examples = ParametersUtil.extractExamples(parameterAnnotations); + OperationParameter parameter = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_RESOURCE, 0, 1, description, examples); parameter.initializeTypes(theMethod, null, null, parameterType); newParams.add(parameter); } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java index 230763bc8a6..d856dfd9598 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/provider/ServerCapabilityStatementProvider.java @@ -27,16 +27,22 @@ import ca.uhn.fhir.rest.server.method.SearchMethodBinding; import ca.uhn.fhir.rest.server.method.SearchParameter; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; +import ca.uhn.fhir.util.ExtensionUtil; import ca.uhn.fhir.util.FhirTerser; +import ca.uhn.fhir.util.HapiExtensions; import com.google.common.collect.TreeMultimap; +import org.apache.commons.text.WordUtils; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseConformance; +import org.hl7.fhir.instance.model.api.IBaseExtension; +import org.hl7.fhir.instance.model.api.IBaseHasExtensions; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import java.util.Date; @@ -83,641 +89,758 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ public class ServerCapabilityStatementProvider implements IServerConformanceProvider { - public static final boolean DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED = true; - private static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProvider.class); - private final FhirContext myContext; - private final RestfulServer myServer; - private final ISearchParamRegistry mySearchParamRegistry; - private final RestfulServerConfiguration myServerConfiguration; - private final IValidationSupport myValidationSupport; - private String myPublisher = "Not provided"; - private boolean myRestResourceRevIncludesEnabled = DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED; - - /** - * Constructor - */ - public ServerCapabilityStatementProvider(RestfulServer theServer) { - myServer = theServer; - myContext = theServer.getFhirContext(); - mySearchParamRegistry = null; - myServerConfiguration = null; - myValidationSupport = null; - } - - /** - * Constructor - */ - public ServerCapabilityStatementProvider(FhirContext theContext, RestfulServerConfiguration theServerConfiguration) { - myContext = theContext; - myServerConfiguration = theServerConfiguration; - mySearchParamRegistry = null; - myServer = null; - myValidationSupport = null; - } - - /** - * Constructor - */ - public ServerCapabilityStatementProvider(RestfulServer theRestfulServer, ISearchParamRegistry theSearchParamRegistry, IValidationSupport theValidationSupport) { - myContext = theRestfulServer.getFhirContext(); - mySearchParamRegistry = theSearchParamRegistry; - myServer = theRestfulServer; - myServerConfiguration = null; - myValidationSupport = theValidationSupport; - } - - private void checkBindingForSystemOps(FhirTerser theTerser, IBase theRest, Set theSystemOps, BaseMethodBinding theMethodBinding) { - RestOperationTypeEnum restOperationType = theMethodBinding.getRestOperationType(); - if (restOperationType.isSystemLevel()) { - String sysOp = restOperationType.getCode(); - if (theSystemOps.contains(sysOp) == false) { - theSystemOps.add(sysOp); - IBase interaction = theTerser.addElement(theRest, "interaction"); - theTerser.addElement(interaction, "code", sysOp); - } - } - } - - - private String conformanceDate(RestfulServerConfiguration theServerConfiguration) { - IPrimitiveType buildDate = theServerConfiguration.getConformanceDate(); - if (buildDate != null && buildDate.getValue() != null) { - try { - return buildDate.getValueAsString(); - } catch (DataFormatException e) { - // fall through - } - } - return InstantDt.withCurrentTime().getValueAsString(); - } - - private RestfulServerConfiguration getServerConfiguration() { - if (myServer != null) { - return myServer.createConfiguration(); - } - return myServerConfiguration; - } - - - /** - * Gets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The - * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. - */ - public String getPublisher() { - return myPublisher; - } - - /** - * Sets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The - * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. - */ - public void setPublisher(String thePublisher) { - myPublisher = thePublisher; - } - - @Override - @Metadata - public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - - HttpServletRequest servletRequest = null; - if (theRequestDetails instanceof ServletRequestDetails) { - servletRequest = ((ServletRequestDetails) theRequestDetails).getServletRequest(); - } - - RestfulServerConfiguration configuration = getServerConfiguration(); - Bindings bindings = configuration.provideBindings(); - - IBaseConformance retVal = (IBaseConformance) myContext.getResourceDefinition("CapabilityStatement").newInstance(); - - FhirTerser terser = myContext.newTerser(); - - TreeMultimap resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser); - - terser.addElement(retVal, "id", UUID.randomUUID().toString()); - terser.addElement(retVal, "name", "RestServer"); - terser.addElement(retVal, "publisher", myPublisher); - terser.addElement(retVal, "date", conformanceDate(configuration)); - terser.addElement(retVal, "fhirVersion", myContext.getVersion().getVersion().getFhirVersionString()); - - ServletContext servletContext = (ServletContext) (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE)); - String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); - terser.addElement(retVal, "implementation.url", serverBase); - terser.addElement(retVal, "implementation.description", configuration.getImplementationDescription()); - terser.addElement(retVal, "kind", "instance"); - terser.addElement(retVal, "software.name", configuration.getServerName()); - terser.addElement(retVal, "software.version", configuration.getServerVersion()); - if (myContext.isFormatXmlSupported()) { - terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW); - terser.addElement(retVal, "format", Constants.FORMAT_XML); - } - if (myContext.isFormatJsonSupported()) { - terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW); - terser.addElement(retVal, "format", Constants.FORMAT_JSON); - } - if (myContext.isFormatRdfSupported()) { - terser.addElement(retVal, "format", Constants.CT_RDF_TURTLE); - terser.addElement(retVal, "format", Constants.FORMAT_TURTLE); - } - terser.addElement(retVal, "status", "active"); - - IBase rest = terser.addElement(retVal, "rest"); - terser.addElement(rest, "mode", "server"); - - Set systemOps = new HashSet<>(); - Set operationNames = new HashSet<>(); - - Map>> resourceToMethods = configuration.collectMethodBindings(); - Map> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype(); - - TreeMultimap resourceNameToIncludes = TreeMultimap.create(); - TreeMultimap resourceNameToRevIncludes = TreeMultimap.create(); - for (Entry>> nextEntry : resourceToMethods.entrySet()) { - String resourceName = nextEntry.getKey(); - for (BaseMethodBinding nextMethod : nextEntry.getValue()) { - if (nextMethod instanceof SearchMethodBinding) { - resourceNameToIncludes.putAll(resourceName, nextMethod.getIncludes()); - resourceNameToRevIncludes.putAll(resourceName, nextMethod.getRevIncludes()); - } - } - - } - - for (Entry>> nextEntry : resourceToMethods.entrySet()) { - - String resourceName = nextEntry.getKey(); - if (nextEntry.getKey().isEmpty() == false) { - Set resourceOps = new HashSet<>(); - IBase resource = terser.addElement(rest, "resource"); - - postProcessRestResource(terser, resource, resourceName); - - RuntimeResourceDefinition def; - FhirContext context = configuration.getFhirContext(); - if (resourceNameToSharedSupertype.containsKey(resourceName)) { - def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName)); - } else { - def = context.getResourceDefinition(resourceName); - } - terser.addElement(resource, "type", def.getName()); - terser.addElement(resource, "profile", def.getResourceProfile(serverBase)); - - for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { - RestOperationTypeEnum resOpCode = nextMethodBinding.getRestOperationType(); - if (resOpCode.isTypeLevel() || resOpCode.isInstanceLevel()) { - String resOp; - resOp = resOpCode.getCode(); - if (resourceOps.contains(resOp) == false) { - resourceOps.add(resOp); - IBase interaction = terser.addElement(resource, "interaction"); - terser.addElement(interaction, "code", resOp); - } - if (RestOperationTypeEnum.VREAD.equals(resOpCode)) { - // vread implies read - resOp = "read"; - if (resourceOps.contains(resOp) == false) { - resourceOps.add(resOp); - IBase interaction = terser.addElement(resource, "interaction"); - terser.addElement(interaction, "code", resOp); - } - } - } - - if (nextMethodBinding.isSupportsConditional()) { - switch (resOpCode) { - case CREATE: - terser.setElement(resource, "conditionalCreate", "true"); - break; - case DELETE: - if (nextMethodBinding.isSupportsConditionalMultiple()) { - terser.setElement(resource, "conditionalDelete", "multiple"); - } else { - terser.setElement(resource, "conditionalDelete", "single"); - } - break; - case UPDATE: - terser.setElement(resource, "conditionalUpdate", "true"); - break; - case HISTORY_INSTANCE: - case HISTORY_SYSTEM: - case HISTORY_TYPE: - case READ: - case SEARCH_SYSTEM: - case SEARCH_TYPE: - case TRANSACTION: - case VALIDATE: - case VREAD: - case METADATA: - case META_ADD: - case META: - case META_DELETE: - case PATCH: - case BATCH: - case ADD_TAGS: - case DELETE_TAGS: - case GET_TAGS: - case GET_PAGE: - case GRAPHQL_REQUEST: - case EXTENDED_OPERATION_SERVER: - case EXTENDED_OPERATION_TYPE: - case EXTENDED_OPERATION_INSTANCE: - default: - break; - } - } - - checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); - - if (nextMethodBinding instanceof SearchMethodBinding) { - SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding; - if (methodBinding.getQueryName() != null) { - String queryName = bindings.getNamedSearchMethodBindingToName().get(methodBinding); - if (operationNames.add(queryName)) { - IBase operation = terser.addElement(rest, "operation"); - terser.addElement(operation, "name", methodBinding.getQueryName()); - terser.addElement(operation, "definition", (getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + queryName)); - } - } - } else if (nextMethodBinding instanceof OperationMethodBinding) { - OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); - // Only add each operation (by name) once - if (operationNames.add(opName)) { - IBase operation = terser.addElement(rest, "operation"); - terser.addElement(operation, "name", methodBinding.getName().substring(1)); - terser.addElement(operation, "definition", getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName); - } - } - - } - - - ISearchParamRegistry serverConfiguration; - if (myServerConfiguration != null) { - serverConfiguration = myServerConfiguration; - } else { - serverConfiguration = myServer.createConfiguration(); - } - - /* - * If we have an explicit registry (which will be the case in the JPA server) we use it as priority, - * but also fill in any gaps using params from the server itself. This makes sure we include - * global params like _lastUpdated - */ - Map searchParams; - ISearchParamRegistry searchParamRegistry; - if (mySearchParamRegistry != null) { - searchParamRegistry = mySearchParamRegistry; - searchParams = new HashMap<>(mySearchParamRegistry.getActiveSearchParams(resourceName)); - for (Entry nextBuiltInSp : serverConfiguration.getActiveSearchParams(resourceName).entrySet()) { - if (nextBuiltInSp.getKey().startsWith("_") && !searchParams.containsKey(nextBuiltInSp.getKey())) { - searchParams.put(nextBuiltInSp.getKey(), nextBuiltInSp.getValue()); - } - } - } else { - searchParamRegistry = serverConfiguration; - searchParams = serverConfiguration.getActiveSearchParams(resourceName); - } - - - for (RuntimeSearchParam next : searchParams.values()) { - IBase searchParam = terser.addElement(resource, "searchParam"); - terser.addElement(searchParam, "name", next.getName()); - terser.addElement(searchParam, "type", next.getParamType().getCode()); - if (isNotBlank(next.getDescription())) { - terser.addElement(searchParam, "documentation", next.getDescription()); - } - - String spUri = next.getUri(); - if (isBlank(spUri) && servletRequest != null) { - String id; - if (next.getId() != null) { - id = next.getId().toUnqualifiedVersionless().getValue(); - } else { - id = resourceName + "-" + next.getName(); - } - spUri = configuration.getServerAddressStrategy().determineServerBase(servletRequest.getServletContext(), servletRequest) + "/" + id; - } - if (isNotBlank(spUri)) { - terser.addElement(searchParam, "definition", spUri); - } - } - - // Add Include to CapabilityStatement.rest.resource - NavigableSet resourceIncludes = resourceNameToIncludes.get(resourceName); - if (resourceIncludes.isEmpty()) { - List includes = searchParams - .values() - .stream() - .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) - .map(t -> resourceName + ":" + t.getName()) - .sorted() - .collect(Collectors.toList()); - terser.addElement(resource, "searchInclude", "*"); - for (String nextInclude : includes) { - terser.addElement(resource, "searchInclude", nextInclude); - } - } else { - for (String resourceInclude : resourceIncludes) { - terser.addElement(resource, "searchInclude", resourceInclude); - } - } - - // Add RevInclude to CapabilityStatement.rest.resource - if (myRestResourceRevIncludesEnabled) { - NavigableSet resourceRevIncludes = resourceNameToRevIncludes.get(resourceName); - if (resourceRevIncludes.isEmpty()) { - TreeSet revIncludes = new TreeSet<>(); - for (String nextResourceName : resourceToMethods.keySet()) { - if (isBlank(nextResourceName)) { - continue; - } - - for (RuntimeSearchParam t : searchParamRegistry.getActiveSearchParams(nextResourceName).values()) { - if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { - if (isNotBlank(t.getName())) { - boolean appropriateTarget = false; - if (t.getTargets().contains(resourceName) || t.getTargets().isEmpty()) { - appropriateTarget = true; - } - - if (appropriateTarget) { - revIncludes.add(nextResourceName + ":" + t.getName()); - } - } - } - } - } - for (String nextInclude : revIncludes) { - terser.addElement(resource, "searchRevInclude", nextInclude); - } - } else { - for (String resourceInclude : resourceRevIncludes) { - terser.addElement(resource, "searchRevInclude", resourceInclude); - } - } - } - - // Add SupportedProfile to CapabilityStatement.rest.resource - for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) { - terser.addElement(resource, "supportedProfile", supportedProfile); - } - - } else { - for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { - checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); - if (nextMethodBinding instanceof OperationMethodBinding) { - OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); - if (operationNames.add(opName)) { - ourLog.debug("Found bound operation: {}", opName); - IBase operation = terser.addElement(rest, "operation"); - terser.addElement(operation, "name", methodBinding.getName().substring(1)); - terser.addElement(operation, "definition", getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + opName); - } - } - } - } - - postProcessRest(terser, rest); - - } - - postProcess(terser, retVal); - - return retVal; - } - - private TreeMultimap getSupportedProfileMultimap(FhirTerser terser) { - TreeMultimap resourceTypeToSupportedProfiles = TreeMultimap.create(); - if (myValidationSupport != null) { - List allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions(); - if (allStructureDefinitions != null) { - for (IBaseResource next : allStructureDefinitions) { - String kind = terser.getSinglePrimitiveValueOrNull(next, "kind"); - String url = terser.getSinglePrimitiveValueOrNull(next, "url"); - String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition")); - if ("resource".equals(kind) && isNotBlank(url)) { - - // Don't include the base resource definitions in the supported profile list - This isn't helpful - if (baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/DomainResource") || baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/Resource")) { - continue; - } - - String resourceType = terser.getSinglePrimitiveValueOrNull(next, "snapshot.element.path"); - if (isBlank(resourceType)) { - resourceType = terser.getSinglePrimitiveValueOrNull(next, "differential.element.path"); - } - - if (isNotBlank(resourceType)) { - resourceTypeToSupportedProfiles.put(resourceType, url); - } - } - } - } - } - return resourceTypeToSupportedProfiles; - } - - /** - * Subclasses may override - */ - protected void postProcess(FhirTerser theTerser, IBaseConformance theCapabilityStatement) { - // nothing - } - - /** - * Subclasses may override - */ - protected void postProcessRest(FhirTerser theTerser, IBase theRest) { - // nothing - } - - /** - * Subclasses may override - */ - protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) { - // nothing - } - - protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) { - if (theRequestDetails == null) { - return ""; - } - return theRequestDetails.getServerBaseForRequest() + "/"; - } - - - @Read(typeName = "OperationDefinition") - public IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) { - if (theId == null || theId.hasIdPart() == false) { - throw new ResourceNotFoundException(theId); - } - RestfulServerConfiguration configuration = getServerConfiguration(); - Bindings bindings = configuration.provideBindings(); - - List operationBindings = bindings.getOperationNameToBindings().get(theId.getIdPart()); - if (operationBindings != null && !operationBindings.isEmpty()) { - return readOperationDefinitionForOperation(operationBindings); - } - List searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart()); - if (searchBindings != null && !searchBindings.isEmpty()) { - return readOperationDefinitionForNamedSearch(searchBindings); - } - throw new ResourceNotFoundException(theId); - } - - private IBaseResource readOperationDefinitionForNamedSearch(List bindings) { - IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); - FhirTerser terser = myContext.newTerser(); - - terser.addElement(op, "status", "active"); - terser.addElement(op, "kind", "query"); - terser.addElement(op, "affectsState", "false"); - - terser.addElement(op, "instance", "false"); - - Set inParams = new HashSet<>(); - - String operationCode = null; - for (SearchMethodBinding binding : bindings) { - if (isNotBlank(binding.getDescription())) { - terser.addElement(op, "description", binding.getDescription()); - } - if (isBlank(binding.getResourceProviderResourceName())) { - terser.addElement(op, "system", "true"); - terser.addElement(op, "type", "false"); - } else { - terser.addElement(op, "system", "false"); - terser.addElement(op, "type", "true"); - terser.addElement(op, "resource", binding.getResourceProviderResourceName()); - } - - if (operationCode == null) { - operationCode = binding.getQueryName(); - } - - for (IParameter nextParamUntyped : binding.getParameters()) { - if (nextParamUntyped instanceof SearchParameter) { - SearchParameter nextParam = (SearchParameter) nextParamUntyped; - if (!inParams.add(nextParam.getName())) { - continue; - } - - IBase param = terser.addElement(op, "parameter"); - terser.addElement(param, "use", "in"); - terser.addElement(param, "type", "string"); - terser.addElement(param, "searchType", nextParam.getParamType().getCode()); - terser.addElement(param, "min", nextParam.isRequired() ? "1" : "0"); - terser.addElement(param, "max", "1"); - terser.addElement(param, "name", nextParam.getName()); - } - } - - } - - terser.addElement(op, "code", operationCode); - terser.addElement(op, "name", "Search_" + operationCode); - - return op; - } - - private IBaseResource readOperationDefinitionForOperation(List bindings) { - IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); - FhirTerser terser = myContext.newTerser(); - - terser.addElement(op, "status", "active"); - terser.addElement(op, "kind", "operation"); - - boolean systemLevel = false; - boolean typeLevel = false; - boolean instanceLevel = false; - boolean affectsState = false; - String description = null; - String code = null; - String name; - - Set resourceNames = new TreeSet<>(); - Set inParams = new HashSet<>(); - Set outParams = new HashSet<>(); - - for (OperationMethodBinding sharedDescription : bindings) { - if (isNotBlank(sharedDescription.getDescription()) && isBlank(description)) { - description = sharedDescription.getDescription(); - } - if (sharedDescription.isCanOperateAtInstanceLevel()) { - instanceLevel = true; - } - if (sharedDescription.isCanOperateAtServerLevel()) { - systemLevel = true; - } - if (sharedDescription.isCanOperateAtTypeLevel()) { - typeLevel = true; - } - if (!sharedDescription.isIdempotent()) { - affectsState |= true; - } - - code = sharedDescription.getName().substring(1); - - if (isNotBlank(sharedDescription.getResourceName())) { - resourceNames.add(sharedDescription.getResourceName()); - } - - for (IParameter nextParamUntyped : sharedDescription.getParameters()) { - if (nextParamUntyped instanceof OperationParameter) { - OperationParameter nextParam = (OperationParameter) nextParamUntyped; - if (!inParams.add(nextParam.getName())) { - continue; - } - IBase param = terser.addElement(op, "parameter"); - terser.addElement(param, "use", "in"); - if (nextParam.getParamType() != null) { - terser.addElement(param, "type", nextParam.getParamType()); - } - if (nextParam.getSearchParamType() != null) { - terser.addElement(param, "searchType", nextParam.getSearchParamType()); - } - terser.addElement(param, "min", Integer.toString(nextParam.getMin())); - terser.addElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); - terser.addElement(param, "name", nextParam.getName()); - } - } - - for (ReturnType nextParam : sharedDescription.getReturnParams()) { - if (!outParams.add(nextParam.getName())) { - continue; - } - IBase param = terser.addElement(op, "parameter"); - terser.addElement(param, "use", "out"); - if (nextParam.getType() != null) { - terser.addElement(param, "type", nextParam.getType()); - } - terser.addElement(param, "min", Integer.toString(nextParam.getMin())); - terser.addElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); - terser.addElement(param, "name", nextParam.getName()); - } - } - - name = "Operation_" + code; - - terser.addElements(op, "resource", resourceNames); - terser.addElement(op, "name", name); - terser.addElement(op, "code", code); - terser.addElement(op, "description", description); - terser.addElement(op, "affectsState", Boolean.toString(affectsState)); - terser.addElement(op, "system", Boolean.toString(systemLevel)); - terser.addElement(op, "type", Boolean.toString(typeLevel)); - terser.addElement(op, "instance", Boolean.toString(instanceLevel)); - - return op; - } - - @Override - public void setRestfulServer(RestfulServer theRestfulServer) { - // ignore - } - - public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) { - myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled; - } + public static final boolean DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED = true; + private static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProvider.class); + private final FhirContext myContext; + private final RestfulServer myServer; + private final ISearchParamRegistry mySearchParamRegistry; + private final RestfulServerConfiguration myServerConfiguration; + private final IValidationSupport myValidationSupport; + private String myPublisher = "Not provided"; + private boolean myRestResourceRevIncludesEnabled = DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED; + + /** + * Constructor + */ + public ServerCapabilityStatementProvider(RestfulServer theServer) { + myServer = theServer; + myContext = theServer.getFhirContext(); + mySearchParamRegistry = null; + myServerConfiguration = null; + myValidationSupport = null; + } + + /** + * Constructor + */ + public ServerCapabilityStatementProvider(FhirContext theContext, RestfulServerConfiguration theServerConfiguration) { + myContext = theContext; + myServerConfiguration = theServerConfiguration; + mySearchParamRegistry = null; + myServer = null; + myValidationSupport = null; + } + + /** + * Constructor + */ + public ServerCapabilityStatementProvider(RestfulServer theRestfulServer, ISearchParamRegistry theSearchParamRegistry, IValidationSupport theValidationSupport) { + myContext = theRestfulServer.getFhirContext(); + mySearchParamRegistry = theSearchParamRegistry; + myServer = theRestfulServer; + myServerConfiguration = null; + myValidationSupport = theValidationSupport; + } + + private void checkBindingForSystemOps(FhirTerser theTerser, IBase theRest, Set theSystemOps, BaseMethodBinding theMethodBinding) { + RestOperationTypeEnum restOperationType = theMethodBinding.getRestOperationType(); + if (restOperationType.isSystemLevel()) { + String sysOp = restOperationType.getCode(); + if (theSystemOps.contains(sysOp) == false) { + theSystemOps.add(sysOp); + IBase interaction = theTerser.addElement(theRest, "interaction"); + theTerser.addElement(interaction, "code", sysOp); + } + } + } + + + private String conformanceDate(RestfulServerConfiguration theServerConfiguration) { + IPrimitiveType buildDate = theServerConfiguration.getConformanceDate(); + if (buildDate != null && buildDate.getValue() != null) { + try { + return buildDate.getValueAsString(); + } catch (DataFormatException e) { + // fall through + } + } + return InstantDt.withCurrentTime().getValueAsString(); + } + + private RestfulServerConfiguration getServerConfiguration() { + if (myServer != null) { + return myServer.createConfiguration(); + } + return myServerConfiguration; + } + + + /** + * Gets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The + * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. + */ + public String getPublisher() { + return myPublisher; + } + + /** + * Sets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The + * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. + */ + public void setPublisher(String thePublisher) { + myPublisher = thePublisher; + } + + @Override + @Metadata + public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { + + HttpServletRequest servletRequest = null; + if (theRequestDetails instanceof ServletRequestDetails) { + servletRequest = ((ServletRequestDetails) theRequestDetails).getServletRequest(); + } + + RestfulServerConfiguration configuration = getServerConfiguration(); + Bindings bindings = configuration.provideBindings(); + + IBaseConformance retVal = (IBaseConformance) myContext.getResourceDefinition("CapabilityStatement").newInstance(); + + FhirTerser terser = myContext.newTerser(); + + TreeMultimap resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser); + + terser.addElement(retVal, "id", UUID.randomUUID().toString()); + terser.addElement(retVal, "name", "RestServer"); + terser.addElement(retVal, "publisher", myPublisher); + terser.addElement(retVal, "date", conformanceDate(configuration)); + terser.addElement(retVal, "fhirVersion", myContext.getVersion().getVersion().getFhirVersionString()); + + ServletContext servletContext = (ServletContext) (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE)); + String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); + terser.addElement(retVal, "implementation.url", serverBase); + terser.addElement(retVal, "implementation.description", configuration.getImplementationDescription()); + terser.addElement(retVal, "kind", "instance"); + if (myServer != null && isNotBlank(myServer.getCopyright())) { + terser.addElement(retVal, "copyright", myServer.getCopyright()); + } + terser.addElement(retVal, "software.name", configuration.getServerName()); + terser.addElement(retVal, "software.version", configuration.getServerVersion()); + if (myContext.isFormatXmlSupported()) { + terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW); + terser.addElement(retVal, "format", Constants.FORMAT_XML); + } + if (myContext.isFormatJsonSupported()) { + terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW); + terser.addElement(retVal, "format", Constants.FORMAT_JSON); + } + if (myContext.isFormatRdfSupported()) { + terser.addElement(retVal, "format", Constants.CT_RDF_TURTLE); + terser.addElement(retVal, "format", Constants.FORMAT_TURTLE); + } + terser.addElement(retVal, "status", "active"); + + IBase rest = terser.addElement(retVal, "rest"); + terser.addElement(rest, "mode", "server"); + + Set systemOps = new HashSet<>(); + + Map>> resourceToMethods = configuration.collectMethodBindings(); + Map> resourceNameToSharedSupertype = configuration.getNameToSharedSupertype(); + List> globalMethodBindings = configuration.getGlobalBindings(); + + TreeMultimap resourceNameToIncludes = TreeMultimap.create(); + TreeMultimap resourceNameToRevIncludes = TreeMultimap.create(); + for (Entry>> nextEntry : resourceToMethods.entrySet()) { + String resourceName = nextEntry.getKey(); + for (BaseMethodBinding nextMethod : nextEntry.getValue()) { + if (nextMethod instanceof SearchMethodBinding) { + resourceNameToIncludes.putAll(resourceName, nextMethod.getIncludes()); + resourceNameToRevIncludes.putAll(resourceName, nextMethod.getRevIncludes()); + } + } + + } + + for (Entry>> nextEntry : resourceToMethods.entrySet()) { + + Set operationNames = new HashSet<>(); + String resourceName = nextEntry.getKey(); + if (nextEntry.getKey().isEmpty() == false) { + Set resourceOps = new HashSet<>(); + IBase resource = terser.addElement(rest, "resource"); + + postProcessRestResource(terser, resource, resourceName); + + RuntimeResourceDefinition def; + FhirContext context = configuration.getFhirContext(); + if (resourceNameToSharedSupertype.containsKey(resourceName)) { + def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName)); + } else { + def = context.getResourceDefinition(resourceName); + } + terser.addElement(resource, "type", def.getName()); + terser.addElement(resource, "profile", def.getResourceProfile(serverBase)); + + for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { + RestOperationTypeEnum resOpCode = nextMethodBinding.getRestOperationType(); + if (resOpCode.isTypeLevel() || resOpCode.isInstanceLevel()) { + String resOp; + resOp = resOpCode.getCode(); + if (resourceOps.contains(resOp) == false) { + resourceOps.add(resOp); + IBase interaction = terser.addElement(resource, "interaction"); + terser.addElement(interaction, "code", resOp); + } + if (RestOperationTypeEnum.VREAD.equals(resOpCode)) { + // vread implies read + resOp = "read"; + if (resourceOps.contains(resOp) == false) { + resourceOps.add(resOp); + IBase interaction = terser.addElement(resource, "interaction"); + terser.addElement(interaction, "code", resOp); + } + } + } + + if (nextMethodBinding.isSupportsConditional()) { + switch (resOpCode) { + case CREATE: + terser.setElement(resource, "conditionalCreate", "true"); + break; + case DELETE: + if (nextMethodBinding.isSupportsConditionalMultiple()) { + terser.setElement(resource, "conditionalDelete", "multiple"); + } else { + terser.setElement(resource, "conditionalDelete", "single"); + } + break; + case UPDATE: + terser.setElement(resource, "conditionalUpdate", "true"); + break; + case HISTORY_INSTANCE: + case HISTORY_SYSTEM: + case HISTORY_TYPE: + case READ: + case SEARCH_SYSTEM: + case SEARCH_TYPE: + case TRANSACTION: + case VALIDATE: + case VREAD: + case METADATA: + case META_ADD: + case META: + case META_DELETE: + case PATCH: + case BATCH: + case ADD_TAGS: + case DELETE_TAGS: + case GET_TAGS: + case GET_PAGE: + case GRAPHQL_REQUEST: + case EXTENDED_OPERATION_SERVER: + case EXTENDED_OPERATION_TYPE: + case EXTENDED_OPERATION_INSTANCE: + default: + break; + } + } + + checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); + + // Resource Operations + if (nextMethodBinding instanceof SearchMethodBinding) { + addSearchMethodIfSearchIsNamedQuery(theRequestDetails, bindings, terser, operationNames, resource, (SearchMethodBinding) nextMethodBinding); + } else if (nextMethodBinding instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; + String opName = bindings.getOperationBindingToId().get(methodBinding); + // Only add each operation (by name) once + if (operationNames.add(opName)) { + IBase operation = terser.addElement(resource, "operation"); + populateOperation(theRequestDetails, terser, methodBinding, opName, operation); + } + } + + } + + // Find any global operations (Operations defines at the system level but with the + // global flag set to true, meaning they apply to all resource types) + if (globalMethodBindings != null) { + Set globalOperationNames = new HashSet<>(); + for (BaseMethodBinding next : globalMethodBindings) { + if (next instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) next; + if (methodBinding.isGlobalMethod()) { + if (methodBinding.isCanOperateAtInstanceLevel() || methodBinding.isCanOperateAtTypeLevel()) { + String opName = bindings.getOperationBindingToId().get(methodBinding); + // Only add each operation (by name) once + if (globalOperationNames.add(opName)) { + IBase operation = terser.addElement(resource, "operation"); + populateOperation(theRequestDetails, terser, methodBinding, opName, operation); + } + } + } + } + } + } + + ISearchParamRegistry serverConfiguration; + if (myServerConfiguration != null) { + serverConfiguration = myServerConfiguration; + } else { + serverConfiguration = myServer.createConfiguration(); + } + + /* + * If we have an explicit registry (which will be the case in the JPA server) we use it as priority, + * but also fill in any gaps using params from the server itself. This makes sure we include + * global params like _lastUpdated + */ + Map searchParams; + ISearchParamRegistry searchParamRegistry; + if (mySearchParamRegistry != null) { + searchParamRegistry = mySearchParamRegistry; + searchParams = new HashMap<>(mySearchParamRegistry.getActiveSearchParams(resourceName)); + for (Entry nextBuiltInSp : serverConfiguration.getActiveSearchParams(resourceName).entrySet()) { + if (nextBuiltInSp.getKey().startsWith("_") && !searchParams.containsKey(nextBuiltInSp.getKey())) { + searchParams.put(nextBuiltInSp.getKey(), nextBuiltInSp.getValue()); + } + } + } else { + searchParamRegistry = serverConfiguration; + searchParams = serverConfiguration.getActiveSearchParams(resourceName); + } + + + for (RuntimeSearchParam next : searchParams.values()) { + IBase searchParam = terser.addElement(resource, "searchParam"); + terser.addElement(searchParam, "name", next.getName()); + terser.addElement(searchParam, "type", next.getParamType().getCode()); + if (isNotBlank(next.getDescription())) { + terser.addElement(searchParam, "documentation", next.getDescription()); + } + + String spUri = next.getUri(); + if (isBlank(spUri) && servletRequest != null) { + String id; + if (next.getId() != null) { + id = next.getId().toUnqualifiedVersionless().getValue(); + } else { + id = resourceName + "-" + next.getName(); + } + spUri = configuration.getServerAddressStrategy().determineServerBase(servletRequest.getServletContext(), servletRequest) + "/" + id; + } + if (isNotBlank(spUri)) { + terser.addElement(searchParam, "definition", spUri); + } + } + + // Add Include to CapabilityStatement.rest.resource + NavigableSet resourceIncludes = resourceNameToIncludes.get(resourceName); + if (resourceIncludes.isEmpty()) { + List includes = searchParams + .values() + .stream() + .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) + .map(t -> resourceName + ":" + t.getName()) + .sorted() + .collect(Collectors.toList()); + terser.addElement(resource, "searchInclude", "*"); + for (String nextInclude : includes) { + terser.addElement(resource, "searchInclude", nextInclude); + } + } else { + for (String resourceInclude : resourceIncludes) { + terser.addElement(resource, "searchInclude", resourceInclude); + } + } + + // Add RevInclude to CapabilityStatement.rest.resource + if (myRestResourceRevIncludesEnabled) { + NavigableSet resourceRevIncludes = resourceNameToRevIncludes.get(resourceName); + if (resourceRevIncludes.isEmpty()) { + TreeSet revIncludes = new TreeSet<>(); + for (String nextResourceName : resourceToMethods.keySet()) { + if (isBlank(nextResourceName)) { + continue; + } + + for (RuntimeSearchParam t : searchParamRegistry.getActiveSearchParams(nextResourceName).values()) { + if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { + if (isNotBlank(t.getName())) { + boolean appropriateTarget = false; + if (t.getTargets().contains(resourceName) || t.getTargets().isEmpty()) { + appropriateTarget = true; + } + + if (appropriateTarget) { + revIncludes.add(nextResourceName + ":" + t.getName()); + } + } + } + } + } + for (String nextInclude : revIncludes) { + terser.addElement(resource, "searchRevInclude", nextInclude); + } + } else { + for (String resourceInclude : resourceRevIncludes) { + terser.addElement(resource, "searchRevInclude", resourceInclude); + } + } + } + + // Add SupportedProfile to CapabilityStatement.rest.resource + for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) { + terser.addElement(resource, "supportedProfile", supportedProfile); + } + + } else { + for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { + checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); + if (nextMethodBinding instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; + if (!methodBinding.isGlobalMethod()) { + String opName = bindings.getOperationBindingToId().get(methodBinding); + if (operationNames.add(opName)) { + ourLog.debug("Found bound operation: {}", opName); + IBase operation = terser.addElement(rest, "operation"); + populateOperation(theRequestDetails, terser, methodBinding, opName, operation); + } + } + } else if (nextMethodBinding instanceof SearchMethodBinding) { + addSearchMethodIfSearchIsNamedQuery(theRequestDetails, bindings, terser, operationNames, rest, (SearchMethodBinding) nextMethodBinding); + } + } + } + + } + + + // Find any global operations (Operations defines at the system level but with the + // global flag set to true, meaning they apply to all resource types) + if (globalMethodBindings != null) { + Set globalOperationNames = new HashSet<>(); + for (BaseMethodBinding next : globalMethodBindings) { + if (next instanceof OperationMethodBinding) { + OperationMethodBinding methodBinding = (OperationMethodBinding) next; + if (methodBinding.isGlobalMethod()) { + if (methodBinding.isCanOperateAtServerLevel()) { + String opName = bindings.getOperationBindingToId().get(methodBinding); + // Only add each operation (by name) once + if (globalOperationNames.add(opName)) { + IBase operation = terser.addElement(rest, "operation"); + populateOperation(theRequestDetails, terser, methodBinding, opName, operation); + } + } + } + } + } + } + + + postProcessRest(terser, rest); + postProcess(terser, retVal); + + return retVal; + } + + private void addSearchMethodIfSearchIsNamedQuery(RequestDetails theRequestDetails, Bindings theBindings, FhirTerser theTerser, Set theOperationNamesAlreadyAdded, IBase theElementToAddTo, SearchMethodBinding theSearchMethodBinding) { + if (theSearchMethodBinding.getQueryName() != null) { + String queryName = theBindings.getNamedSearchMethodBindingToName().get(theSearchMethodBinding); + if (theOperationNamesAlreadyAdded.add(queryName)) { + IBase operation = theTerser.addElement(theElementToAddTo, "operation"); + theTerser.addElement(operation, "name", theSearchMethodBinding.getQueryName()); + theTerser.addElement(operation, "definition", (createOperationUrl(theRequestDetails, queryName))); + } + } + } + + private void populateOperation(RequestDetails theRequestDetails, FhirTerser theTerser, OperationMethodBinding theMethodBinding, String theOpName, IBase theOperation) { + String operationName = theMethodBinding.getName().substring(1); + theTerser.addElement(theOperation, "name", operationName); + theTerser.addElement(theOperation, "definition", createOperationUrl(theRequestDetails, theOpName)); + if (isNotBlank(theMethodBinding.getDescription())) { + theTerser.addElement(theOperation, "documentation", theMethodBinding.getDescription()); + } + } + + @Nonnull + private String createOperationUrl(RequestDetails theRequestDetails, String theOpName) { + return getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + theOpName; + } + + private TreeMultimap getSupportedProfileMultimap(FhirTerser terser) { + TreeMultimap resourceTypeToSupportedProfiles = TreeMultimap.create(); + if (myValidationSupport != null) { + List allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions(); + if (allStructureDefinitions != null) { + for (IBaseResource next : allStructureDefinitions) { + String kind = terser.getSinglePrimitiveValueOrNull(next, "kind"); + String url = terser.getSinglePrimitiveValueOrNull(next, "url"); + String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition")); + if ("resource".equals(kind) && isNotBlank(url)) { + + // Don't include the base resource definitions in the supported profile list - This isn't helpful + if (baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/DomainResource") || baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/Resource")) { + continue; + } + + String resourceType = terser.getSinglePrimitiveValueOrNull(next, "snapshot.element.path"); + if (isBlank(resourceType)) { + resourceType = terser.getSinglePrimitiveValueOrNull(next, "differential.element.path"); + } + + if (isNotBlank(resourceType)) { + resourceTypeToSupportedProfiles.put(resourceType, url); + } + } + } + } + } + return resourceTypeToSupportedProfiles; + } + + /** + * Subclasses may override + */ + protected void postProcess(FhirTerser theTerser, IBaseConformance theCapabilityStatement) { + // nothing + } + + /** + * Subclasses may override + */ + protected void postProcessRest(FhirTerser theTerser, IBase theRest) { + // nothing + } + + /** + * Subclasses may override + */ + protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) { + // nothing + } + + protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) { + if (theRequestDetails == null) { + return ""; + } + return theRequestDetails.getServerBaseForRequest() + "/"; + } + + + @Override + @Read(typeName = "OperationDefinition") + public IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) { + if (theId == null || theId.hasIdPart() == false) { + throw new ResourceNotFoundException(theId); + } + RestfulServerConfiguration configuration = getServerConfiguration(); + Bindings bindings = configuration.provideBindings(); + + List operationBindings = bindings.getOperationIdToBindings().get(theId.getIdPart()); + if (operationBindings != null && !operationBindings.isEmpty()) { + return readOperationDefinitionForOperation(theRequestDetails, bindings, operationBindings); + } + + List searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart()); + if (searchBindings != null && !searchBindings.isEmpty()) { + return readOperationDefinitionForNamedSearch(searchBindings); + } + throw new ResourceNotFoundException(theId); + } + + private IBaseResource readOperationDefinitionForNamedSearch(List bindings) { + IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); + FhirTerser terser = myContext.newTerser(); + + terser.addElement(op, "status", "active"); + terser.addElement(op, "kind", "query"); + terser.addElement(op, "affectsState", "false"); + + terser.addElement(op, "instance", "false"); + + Set inParams = new HashSet<>(); + + String operationCode = null; + for (SearchMethodBinding binding : bindings) { + if (isNotBlank(binding.getDescription())) { + terser.addElement(op, "description", binding.getDescription()); + } + if (isBlank(binding.getResourceProviderResourceName())) { + terser.addElement(op, "system", "true"); + terser.addElement(op, "type", "false"); + } else { + terser.addElement(op, "system", "false"); + terser.addElement(op, "type", "true"); + terser.addElement(op, "resource", binding.getResourceProviderResourceName()); + } + + if (operationCode == null) { + operationCode = binding.getQueryName(); + } + + for (IParameter nextParamUntyped : binding.getParameters()) { + if (nextParamUntyped instanceof SearchParameter) { + SearchParameter nextParam = (SearchParameter) nextParamUntyped; + if (!inParams.add(nextParam.getName())) { + continue; + } + + IBase param = terser.addElement(op, "parameter"); + terser.addElement(param, "use", "in"); + terser.addElement(param, "type", "string"); + terser.addElement(param, "searchType", nextParam.getParamType().getCode()); + terser.addElement(param, "min", nextParam.isRequired() ? "1" : "0"); + terser.addElement(param, "max", "1"); + terser.addElement(param, "name", nextParam.getName()); + } + } + + } + + terser.addElement(op, "code", operationCode); + + String operationName = WordUtils.capitalize(operationCode); + terser.addElement(op, "name", operationName); + + return op; + } + + private IBaseResource readOperationDefinitionForOperation(RequestDetails theRequestDetails, Bindings theBindings, List theOperationMethodBindings) { + IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); + FhirTerser terser = myContext.newTerser(); + + terser.addElement(op, "status", "active"); + terser.addElement(op, "kind", "operation"); + + boolean systemLevel = false; + boolean typeLevel = false; + boolean instanceLevel = false; + boolean affectsState = false; + String description = null; + String title = null; + String code = null; + String url = null; + + Set resourceNames = new TreeSet<>(); + Map inParams = new HashMap<>(); + Map outParams = new HashMap<>(); + + for (OperationMethodBinding operationMethodBinding : theOperationMethodBindings) { + if (isNotBlank(operationMethodBinding.getDescription()) && isBlank(description)) { + description = operationMethodBinding.getDescription(); + } + if (isNotBlank(operationMethodBinding.getShortDescription()) && isBlank(title)) { + title = operationMethodBinding.getShortDescription(); + } + if (operationMethodBinding.isCanOperateAtInstanceLevel()) { + instanceLevel = true; + } + if (operationMethodBinding.isCanOperateAtServerLevel()) { + systemLevel = true; + } + if (operationMethodBinding.isCanOperateAtTypeLevel()) { + typeLevel = true; + } + if (!operationMethodBinding.isIdempotent()) { + affectsState |= true; + } + + code = operationMethodBinding.getName().substring(1); + + if (isNotBlank(operationMethodBinding.getResourceName())) { + resourceNames.add(operationMethodBinding.getResourceName()); + } + + if (isBlank(url)) { + url = theBindings.getOperationBindingToId().get(operationMethodBinding); + if (isNotBlank(url)) { + url = createOperationUrl(theRequestDetails, url); + } + } + + + for (IParameter nextParamUntyped : operationMethodBinding.getParameters()) { + if (nextParamUntyped instanceof OperationParameter) { + OperationParameter nextParam = (OperationParameter) nextParamUntyped; + + IBase param = inParams.get(nextParam.getName()); + if (param == null){ + param = terser.addElement(op, "parameter"); + inParams.put(nextParam.getName(), param); + } + + IBase existingParam = inParams.get(nextParam.getName()); + if (isNotBlank(nextParam.getDescription()) && terser.getValues(existingParam, "documentation").isEmpty()) { + terser.addElement(existingParam, "documentation", nextParam.getDescription()); + } + + if (nextParam.getParamType() != null) { + String existingType = terser.getSinglePrimitiveValueOrNull(existingParam, "type"); + if (!nextParam.getParamType().equals(existingType)) { + if (existingType == null) { + terser.setElement(existingParam, "type", nextParam.getParamType()); + } else { + terser.setElement(existingParam, "type", "Resource"); + } + } + } + + terser.setElement(param, "use", "in"); + if (nextParam.getSearchParamType() != null) { + terser.setElement(param, "searchType", nextParam.getSearchParamType()); + } + terser.setElement(param, "min", Integer.toString(nextParam.getMin())); + terser.setElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); + terser.setElement(param, "name", nextParam.getName()); + + List> existingExampleExtensions = ExtensionUtil.getExtensionsByUrl((IBaseHasExtensions) param, HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE); + Set existingExamples = existingExampleExtensions + .stream() + .map(t -> t.getValue()) + .filter(t -> t != null) + .map(t -> (IPrimitiveType) t) + .map(t -> t.getValueAsString()) + .collect(Collectors.toSet()); + for (String nextExample : nextParam.getExampleValues()) { + if (!existingExamples.contains(nextExample)) { + ExtensionUtil.addExtension(myContext, param, HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE, "string", nextExample); + } + } + + } + } + + for (ReturnType nextParam : operationMethodBinding.getReturnParams()) { + if (outParams.containsKey(nextParam.getName())) { + continue; + } + + IBase param = terser.addElement(op, "parameter"); + outParams.put(nextParam.getName(), param); + + terser.addElement(param, "use", "out"); + if (nextParam.getType() != null) { + terser.addElement(param, "type", nextParam.getType()); + } + terser.addElement(param, "min", Integer.toString(nextParam.getMin())); + terser.addElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); + terser.addElement(param, "name", nextParam.getName()); + } + } + String name = WordUtils.capitalize(code); + + terser.addElements(op, "resource", resourceNames); + terser.addElement(op, "name", name); + terser.addElement(op, "url", url); + terser.addElement(op, "code", code); + terser.addElement(op, "description", description); + terser.addElement(op, "title", title); + terser.addElement(op, "affectsState", Boolean.toString(affectsState)); + terser.addElement(op, "system", Boolean.toString(systemLevel)); + terser.addElement(op, "type", Boolean.toString(typeLevel)); + terser.addElement(op, "instance", Boolean.toString(instanceLevel)); + + return op; + } + + @Override + public void setRestfulServer(RestfulServer theRestfulServer) { + // ignore + } + + public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) { + myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled; + } } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java index fa5ac4e117c..8d90901b141 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRequestDetails.java @@ -49,11 +49,32 @@ public class ServletRequestDetails extends RequestDetails { private HttpServletRequest myServletRequest; private HttpServletResponse myServletResponse; + /** + * Constructor for testing only + */ + public ServletRequestDetails() { + this((IInterceptorBroadcaster)null); + } + + /** + * Constructor + */ public ServletRequestDetails(IInterceptorBroadcaster theInterceptorBroadcaster) { super(theInterceptorBroadcaster); setResponse(new ServletRestfulResponse(this)); } + /** + * Copy constructor + */ + public ServletRequestDetails(ServletRequestDetails theRequestDetails) { + super(theRequestDetails); + + myServer = theRequestDetails.getServer(); + myServletRequest = theRequestDetails.getServletRequest(); + myServletResponse = theRequestDetails.getServletResponse(); + } + @Override protected byte[] getByteStreamRequestContents() { try { diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/SearchMethodBindingTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/SearchMethodBindingTest.java index c2f3add03e5..0a79048cb87 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/SearchMethodBindingTest.java +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/method/SearchMethodBindingTest.java @@ -104,7 +104,7 @@ public class SearchMethodBindingTest { when(requestDetails.getParameters()).thenReturn(params); when(requestDetails.getUnqualifiedToQualifiedNames()).thenAnswer(t -> { - RequestDetails rd = new ServletRequestDetails(null); + RequestDetails rd = new ServletRequestDetails(); rd.setParameters(params); return rd.getUnqualifiedToQualifiedNames(); }); 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 19eedc6bfd2..db762d268b7 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-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 656d16080d7..d0ef59739a9 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-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 0f9be244025..c7dba5afe65 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT hapi-fhir-spring-boot-sample-client-okhttp 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 9d5b6483f92..fd0b2400bd2 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT hapi-fhir-spring-boot-sample-server-jersey 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 a57c5e80e1b..23dc4f4cc96 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT hapi-fhir-spring-boot-samples 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 3f90ae6bbcc..37bb5fbff89 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-spring-boot/pom.xml b/hapi-fhir-spring-boot/pom.xml index ff3fe4a179a..e3050e67351 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-structures-dstu2.1/pom.xml b/hapi-fhir-structures-dstu2.1/pom.xml index 61d4ea265db..143a1fd9ffc 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2.1/src/main/java/org/hl7/fhir/dstu2016may/hapi/rest/server/ServerConformanceProvider.java b/hapi-fhir-structures-dstu2.1/src/main/java/org/hl7/fhir/dstu2016may/hapi/rest/server/ServerConformanceProvider.java index 31045cdab6d..d9122d3f71e 100644 --- a/hapi-fhir-structures-dstu2.1/src/main/java/org/hl7/fhir/dstu2016may/hapi/rest/server/ServerConformanceProvider.java +++ b/hapi-fhir-structures-dstu2.1/src/main/java/org/hl7/fhir/dstu2016may/hapi/rest/server/ServerConformanceProvider.java @@ -275,7 +275,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv handleSearchMethodBinding(rest, resource, resourceName, def, includes, (SearchMethodBinding) nextMethodBinding, theRequestDetails); } else if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { // Only add each operation (by name) once rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition(new Reference("OperationDefinition/" + opName)); @@ -310,7 +310,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv checkBindingForSystemOps(rest, systemOps, nextMethodBinding); if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { ourLog.debug("Found bound operation: {}", opName); rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition(new Reference("OperationDefinition/" + opName)); @@ -419,7 +419,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv RestfulServerConfiguration serverConfiguration = getServerConfiguration(theRequestDetails); Bindings bindings = serverConfiguration.provideBindings(); - List sharedDescriptions = bindings.getOperationNameToBindings().get(theId.getIdPart()); + List sharedDescriptions = bindings.getOperationIdToBindings().get(theId.getIdPart()); if (sharedDescriptions == null || sharedDescriptions.isEmpty()) { throw new ResourceNotFoundException(theId); } diff --git a/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2_1Test.java b/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2_1Test.java index f4bcddd6bbb..93970cb0ca7 100644 --- a/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2_1Test.java +++ b/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2_1Test.java @@ -98,7 +98,7 @@ public class OperationServerDstu2_1Test { */ @Test public void testOperationDefinition() { - OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient--OP_TYPE").execute(); + OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient-t-OP_TYPE").execute(); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(def)); diff --git a/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2_1Test.java b/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2_1Test.java index 8dbb9066dd8..63be245658c 100644 --- a/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2_1Test.java +++ b/hapi-fhir-structures-dstu2.1/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2_1Test.java @@ -193,7 +193,7 @@ public class OperationServerWithSearchParamTypesDstu2_1Test { /* * Check the operation definitions themselves */ - OperationDefinition andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--andlist"), createRequestDetails(rs)); + OperationDefinition andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-andlist"), createRequestDetails(rs)); String def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(andListDef); ourLog.info(def); //@formatter:off @@ -209,7 +209,7 @@ public class OperationServerWithSearchParamTypesDstu2_1Test { )); //@formatter:on - andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--andlist-withnomax"), createRequestDetails(rs)); + andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-andlist-withnomax"), createRequestDetails(rs)); def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(andListDef); ourLog.info(def); //@formatter:off @@ -225,7 +225,7 @@ public class OperationServerWithSearchParamTypesDstu2_1Test { )); //@formatter:on - OperationDefinition orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--orlist"), createRequestDetails(rs)); + OperationDefinition orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-orlist"), createRequestDetails(rs)); def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(orListDef); ourLog.info(def); //@formatter:off @@ -241,7 +241,7 @@ public class OperationServerWithSearchParamTypesDstu2_1Test { )); //@formatter:on - orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--orlist-withnomax"), createRequestDetails(rs)); + orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-orlist-withnomax"), createRequestDetails(rs)); def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(orListDef); ourLog.info(def); //@formatter:off @@ -505,7 +505,7 @@ public class OperationServerWithSearchParamTypesDstu2_1Test { private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); + ServletRequestDetails retVal = new ServletRequestDetails(); retVal.setServer(theServer); return retVal; } diff --git a/hapi-fhir-structures-dstu2/pom.xml b/hapi-fhir-structures-dstu2/pom.xml index 9359dc2e980..eba9cfc2801 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java index cf51eda513a..79d0b6fc093 100644 --- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java +++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/rest/server/provider/dstu2/ServerConformanceProvider.java @@ -271,7 +271,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv handleSearchMethodBinding(resource, def, includes, (SearchMethodBinding) nextMethodBinding, theRequestDetails); } else if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { // Only add each operation (by name) once rest.addOperation().setName(methodBinding.getName().substring(1)).getDefinition().setReference("OperationDefinition/" + opName); @@ -306,7 +306,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv checkBindingForSystemOps(rest, systemOps, nextMethodBinding); if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { rest.addOperation().setName(methodBinding.getName().substring(1)).getDefinition().setReference("OperationDefinition/" + opName); } @@ -415,7 +415,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv RestfulServerConfiguration serverConfiguration = getServerConfiguration(theRequestDetails); Bindings bindings = serverConfiguration.provideBindings(); - List sharedDescriptions = bindings.getOperationNameToBindings().get(theId.getIdPart()); + List sharedDescriptions = bindings.getOperationIdToBindings().get(theId.getIdPart()); if (sharedDescriptions == null || sharedDescriptions.isEmpty()) { throw new ResourceNotFoundException(theId); } @@ -449,10 +449,10 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv for (IParameter nextParamUntyped : sharedDescription.getParameters()) { if (nextParamUntyped instanceof OperationParameter) { OperationParameter nextParam = (OperationParameter) nextParamUntyped; - Parameter param = op.addParameter(); if (!inParams.add(nextParam.getName())) { continue; } + Parameter param = op.addParameter(); param.setUse(OperationParameterUseEnum.IN); if (nextParam.getParamType() != null) { param.setType(nextParam.getParamType()); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java index d2681d3376b..40ada003db2 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java @@ -1134,7 +1134,7 @@ public class XmlParserDstu2Test { ourLog.info(string); parsed = parser.parseResource(Composition.class, string); - assertEquals(2, parsed.getContained().getContainedResources().size()); + assertEquals(3, parsed.getContained().getContainedResources().size()); } /** @@ -1159,7 +1159,7 @@ public class XmlParserDstu2Test { ourLog.info(string); parsed = parser.parseResource(Composition.class, string); - assertEquals(2, parsed.getContained().getContainedResources().size()); + assertEquals(3, parsed.getContained().getContainedResources().size()); } @Test diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerDstu2Test.java index b042b34659b..db916678122 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerDstu2Test.java @@ -50,14 +50,14 @@ public class OperationDuplicateServerDstu2Test { ourLog.info(response); Conformance resp = ourCtx.newXmlParser().parseResource(Conformance.class, response); - assertEquals(3, resp.getRest().get(0).getOperation().size()); + assertEquals(1, resp.getRest().get(0).getOperation().size()); assertEquals("myoperation", resp.getRest().get(0).getOperation().get(0).getName()); - assertEquals("OperationDefinition/-s-myoperation", resp.getRest().get(0).getOperation().get(0).getDefinition().getReference().getValue()); + assertEquals("OperationDefinition/OrganizationPatient-ts-myoperation", resp.getRest().get(0).getOperation().get(0).getDefinition().getReference().getValue()); } // OperationDefinition { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/OperationDefinition/-s-myoperation?_pretty=true"); + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/OperationDefinition/OrganizationPatient-ts-myoperation?_pretty=true"); HttpResponse status = ourClient.execute(httpGet); assertEquals(200, status.getStatusLine().getStatusCode()); @@ -69,25 +69,7 @@ public class OperationDuplicateServerDstu2Test { assertEquals(true, resp.getSystemElement().getValue().booleanValue()); assertEquals("myoperation", resp.getCode()); assertEquals(true, resp.getIdempotent().booleanValue()); - assertEquals(0, resp.getType().size()); - assertEquals(1, resp.getParameter().size()); - } - // OperationDefinition - { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/OperationDefinition/Organization--myoperation?_pretty=true"); - HttpResponse status = ourClient.execute(httpGet); - - assertEquals(200, status.getStatusLine().getStatusCode()); - String response = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); - ourLog.info(response); - - OperationDefinition resp = ourCtx.newXmlParser().parseResource(OperationDefinition.class, response); - assertEquals(false, resp.getSystemElement().getValue().booleanValue()); - assertEquals("myoperation", resp.getCode()); - assertEquals(true, resp.getIdempotent().booleanValue()); - assertEquals(1, resp.getType().size()); - assertEquals("Organization", resp.getType().get(0).getValue()); + assertEquals(2, resp.getType().size()); assertEquals(1, resp.getParameter().size()); } } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java index b6f51ed2243..4dfa2813cc4 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java @@ -105,7 +105,7 @@ public class OperationServerDstu2Test { List opNames = toOpNames(ops); assertThat(opNames, containsInRelativeOrder("OP_TYPE")); - assertEquals("OperationDefinition/Patient--OP_TYPE", ops.get(opNames.indexOf("OP_TYPE")).getDefinition().getReference().getValue()); + assertEquals("OperationDefinition/Patient-t-OP_TYPE", ops.get(opNames.indexOf("OP_TYPE")).getDefinition().getReference().getValue()); } /** @@ -113,7 +113,7 @@ public class OperationServerDstu2Test { */ @Test public void testOperationDefinition() { - OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient--OP_TYPE").execute(); + OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient-t-OP_TYPE").execute(); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(def)); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2Test.java index 837db498fd1..5b7ff389697 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu2Test.java @@ -182,7 +182,7 @@ public class OperationServerWithSearchParamTypesDstu2Test { /* * Check the operation definitions themselves */ - OperationDefinition andListDef = sc.readOperationDefinition(new IdDt("OperationDefinition/Patient--andlist"), createRequestDetails(rs)); + OperationDefinition andListDef = sc.readOperationDefinition(new IdDt("OperationDefinition/Patient-t-andlist"), createRequestDetails(rs)); String def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(andListDef); ourLog.info(def); //@formatter:off @@ -418,7 +418,7 @@ public class OperationServerWithSearchParamTypesDstu2Test { } private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); + ServletRequestDetails retVal = new ServletRequestDetails(); retVal.setServer(theServer); return retVal; } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderDstu2Test.java index a0be908d5ce..1d6bf3f7930 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderDstu2Test.java @@ -25,7 +25,21 @@ import ca.uhn.fhir.model.primitive.DateDt; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.model.primitive.UriDt; -import ca.uhn.fhir.rest.annotation.*; +import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; +import ca.uhn.fhir.rest.annotation.Create; +import ca.uhn.fhir.rest.annotation.Delete; +import ca.uhn.fhir.rest.annotation.History; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.IncludeParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.OptionalParam; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.annotation.RequiredParam; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.annotation.Update; +import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -67,9 +81,9 @@ import static org.mockito.Mockito.when; public class ServerConformanceProviderDstu2Test { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerConformanceProviderDstu2Test.class); private static FhirContext ourCtx; private static FhirValidator ourValidator; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerConformanceProviderDstu2Test.class); static { ourCtx = FhirContext.forDstu2(); @@ -85,7 +99,7 @@ public class ServerConformanceProviderDstu2Test { ValidationResult result = ourValidator.validateWithResult(theOpDef); String outcome = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome()); ourLog.info("Outcome: {}", outcome); - + assertTrue(result.isSuccessful(), outcome); } @@ -97,6 +111,7 @@ public class ServerConformanceProviderDstu2Test { when(req.getContextPath()).thenReturn("/FhirStorm"); return req; } + private ServletConfig createServletConfig() { ServletConfig sc = mock(ServletConfig.class); when(sc.getServletContext()).thenReturn(null); @@ -255,7 +270,9 @@ public class ServerConformanceProviderDstu2Test { assertNull(res.getConditionalUpdate()); } - /** See #379 */ + /** + * See #379 + */ @Test public void testOperationAcrossMultipleTypes() throws Exception { RestfulServer rs = new RestfulServer(ourCtx); @@ -271,55 +288,26 @@ public class ServerConformanceProviderDstu2Test { String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - assertEquals(4, conformance.getRest().get(0).getOperation().size()); + assertEquals(2, conformance.getRest().get(0).getOperation().size()); List operationNames = toOperationNames(conformance.getRest().get(0).getOperation()); - assertThat(operationNames, containsInAnyOrder("someOp", "validate", "someOp", "validate")); - + assertThat(operationNames, containsInAnyOrder("someOp", "validate")); + List operationIdParts = toOperationIdParts(conformance.getRest().get(0).getOperation()); - assertThat(operationIdParts, containsInAnyOrder("Patient-i-someOp","Encounter-i-someOp","Patient-i-validate","Encounter-i-validate")); - + assertThat(operationIdParts, containsInAnyOrder("EncounterPatient-i-someOp", "EncounterPatient-i-validate")); + { - OperationDefinition opDef = sc.readOperationDefinition(new IdDt("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); - validate(opDef); - - Set types = toStrings(opDef.getType()); - assertEquals("someOp", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); - assertEquals(2, opDef.getParameter().size()); - assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); - assertEquals("date", opDef.getParameter().get(0).getType()); - assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Patient", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = sc.readOperationDefinition(new IdDt("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); + OperationDefinition opDef = sc.readOperationDefinition(new IdDt("OperationDefinition/EncounterPatient-i-someOp"), createRequestDetails(rs)); validate(opDef); Set types = toStrings(opDef.getType()); assertEquals("someOp", opDef.getCode()); assertEquals(true, opDef.getInstance()); assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Encounter")); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); assertEquals(2, opDef.getParameter().size()); assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); assertEquals("date", opDef.getParameter().get(0).getType()); assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Encounter", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = sc.readOperationDefinition(new IdDt("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); - validate(opDef); - - Set types = toStrings(opDef.getType()); - assertEquals("validate", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); - assertEquals(1, opDef.getParameter().size()); - assertEquals("resource", opDef.getParameter().get(0).getName()); - assertEquals("Patient", opDef.getParameter().get(0).getType()); } } @@ -344,45 +332,9 @@ public class ServerConformanceProviderDstu2Test { } - @Test - public void testOperationOnNoTypes() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); - rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); - - ServerConformanceProvider sc = new ServerConformanceProvider(rs) { - @Override - public Conformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - return super.getServerConformance(theRequest, theRequestDetails); - } - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - Conformance sconf = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - assertEquals("OperationDefinition/-is-plain", sconf.getRest().get(0).getOperation().get(0).getDefinition().getReference().getValue()); - - OperationDefinition opDef = sc.readOperationDefinition(new IdDt("OperationDefinition/-is-plain"), createRequestDetails(rs)); - validate(opDef); - - assertEquals("plain", opDef.getCode()); - assertEquals(true, opDef.getIdempotent().booleanValue()); - assertEquals(3, opDef.getParameter().size()); - assertEquals("start", opDef.getParameter().get(0).getName()); - assertEquals("in", opDef.getParameter().get(0).getUse()); - assertEquals("0", opDef.getParameter().get(0).getMinElement().getValueAsString()); - assertEquals("date", opDef.getParameter().get(0).getTypeElement().getValueAsString()); - - assertEquals("out1", opDef.getParameter().get(2).getName()); - assertEquals("out", opDef.getParameter().get(2).getUse()); - assertEquals("1", opDef.getParameter().get(2).getMinElement().getValueAsString()); - assertEquals("2", opDef.getParameter().get(2).getMaxElement().getValueAsString()); - assertEquals("string", opDef.getParameter().get(2).getTypeElement().getValueAsString()); - } - @Test public void testProviderForSmart() throws ServletException { - + RestfulServer rs = new RestfulServer(ourCtx); rs.createConfiguration(); rs.setProviders(new ProviderWithRequiredAndOptional()); @@ -391,28 +343,28 @@ public class ServerConformanceProviderDstu2Test { @Override public Conformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { Conformance conformance = super.getServerConformance(theRequest, theRequestDetails); - ExtensionDt extensionDt = new ExtensionDt(); - ExtensionDt extensionDtToken = new ExtensionDt(); - ExtensionDt extensionDtAuthorize = new ExtensionDt(); - Rest rest = conformance.getRestFirstRep(); - RestSecurity restSecurity = rest.getSecurity(); - - conformance.setAcceptUnknown(UnknownContentCodeEnum.UNKNOWN_ELEMENTS_AND_EXTENSIONS); - restSecurity.addService(RestfulSecurityServiceEnum.SMART_ON_FHIR); - restSecurity.getServiceFirstRep().setText("OAuth2 using SMART-on-FHIR profile (see http://docs.smarthealthit.org)"); - extensionDt.setUrl("http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris"); - extensionDtToken.setUrl("token"); - extensionDtToken.setValue(new UriDt("https://SERVERNAME/token")); - extensionDtAuthorize.setUrl("authorize"); - extensionDtAuthorize.setValue(new UriDt("https://SERVERNAME/authorize")); - extensionDt.addUndeclaredExtension(extensionDtToken); + ExtensionDt extensionDt = new ExtensionDt(); + ExtensionDt extensionDtToken = new ExtensionDt(); + ExtensionDt extensionDtAuthorize = new ExtensionDt(); + Rest rest = conformance.getRestFirstRep(); + RestSecurity restSecurity = rest.getSecurity(); + + conformance.setAcceptUnknown(UnknownContentCodeEnum.UNKNOWN_ELEMENTS_AND_EXTENSIONS); + restSecurity.addService(RestfulSecurityServiceEnum.SMART_ON_FHIR); + restSecurity.getServiceFirstRep().setText("OAuth2 using SMART-on-FHIR profile (see http://docs.smarthealthit.org)"); + extensionDt.setUrl("http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris"); + extensionDtToken.setUrl("token"); + extensionDtToken.setValue(new UriDt("https://SERVERNAME/token")); + extensionDtAuthorize.setUrl("authorize"); + extensionDtAuthorize.setValue(new UriDt("https://SERVERNAME/authorize")); + extensionDt.addUndeclaredExtension(extensionDtToken); extensionDt.addUndeclaredExtension(extensionDtAuthorize); restSecurity.addUndeclaredExtension(extensionDt); - + return conformance; } }; - + rs.init(createServletConfig()); Conformance conformance = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); @@ -561,8 +513,8 @@ public class ServerConformanceProviderDstu2Test { ourLog.info(conf); } - - + + /** * See #286 */ @@ -595,7 +547,7 @@ public class ServerConformanceProviderDstu2Test { ourLog.info(conf); RestResource resource = findRestResource(conformance, "Patient"); - + RestResourceSearchParam param = resource.getSearchParam().get(0); assertEquals("bar", param.getChain().get(0).getValue()); assertEquals("foo", param.getChain().get(1).getValue()); @@ -696,7 +648,7 @@ public class ServerConformanceProviderDstu2Test { ValidationResult result = ourCtx.newValidator().validateWithResult(conformance); assertTrue(result.isSuccessful(), result.getMessages().toString()); } - + private List toOperationIdParts(List theOperation) { ArrayList retVal = Lists.newArrayList(); for (RestOperation next : theOperation) { @@ -721,9 +673,10 @@ public class ServerConformanceProviderDstu2Test { return retVal; } - @AfterAll - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); + private RequestDetails createRequestDetails(RestfulServer theServer) { + ServletRequestDetails retVal = new ServletRequestDetails(); + retVal.setServer(theServer); + return retVal; } public static class ConditionalProvider implements IResourceProvider { @@ -779,7 +732,7 @@ public class ServerConformanceProviderDstu2Test { @Operation(name = "someOp") public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam IdDt theId, - @OperationParam(name = "someOpParam1") DateDt theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) { + @OperationParam(name = "someOpParam1") DateDt theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) { return null; } @@ -799,7 +752,7 @@ public class ServerConformanceProviderDstu2Test { @Operation(name = "someOp") public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam IdDt theId, - @OperationParam(name = "someOpParam1") DateDt theStart, @OperationParam(name = "someOpParam2") Patient theEnd) { + @OperationParam(name = "someOpParam1") DateDt theStart, @OperationParam(name = "someOpParam2") Patient theEnd) { return null; } @@ -841,7 +794,7 @@ public class ServerConformanceProviderDstu2Test { public static class PlainProviderWithExtendedOperationOnNoType { - @Operation(name = "plain", idempotent = true, returnParameters = { @OperationParam(min = 1, max = 2, name = "out1", type = StringDt.class) }) + @Operation(name = "plain", idempotent = true, returnParameters = {@OperationParam(min = 1, max = 2, name = "out1", type = StringDt.class)}) public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam ca.uhn.fhir.model.primitive.IdDt theId, @OperationParam(name = "start") DateDt theStart, @OperationParam(name = "end") DateDt theEnd) { return null; } @@ -867,7 +820,7 @@ public class ServerConformanceProviderDstu2Test { @Description(shortDefinition = "This is a search for stuff!") @Search public List findDiagnosticReportsByPatient(@RequiredParam(name = DiagnosticReport.SP_SUBJECT + '.' + Patient.SP_IDENTIFIER) IdentifierDt thePatientId, @OptionalParam(name = DiagnosticReport.SP_CODE) TokenOrListParam theNames, - @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange, @IncludeParam(allow = { "DiagnosticReport.result" }) Set theIncludes) throws Exception { + @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange, @IncludeParam(allow = {"DiagnosticReport.result"}) Set theIncludes) throws Exception { return null; } @@ -895,7 +848,7 @@ public class ServerConformanceProviderDstu2Test { } @Search(type = Patient.class) - public Patient findPatient2(@Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = { Patient.class }) ReferenceAndListParam theLink) { + public Patient findPatient2(@Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = {Patient.class}) ReferenceAndListParam theLink) { return null; } @@ -905,8 +858,8 @@ public class ServerConformanceProviderDstu2Test { @Search(type = Patient.class) public Patient findPatient1( - @Description(shortDefinition = "The organization at which this person is a patient") - @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist= {"foo", "bar"}) + @Description(shortDefinition = "The organization at which this person is a patient") + @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist = {"foo", "bar"}) ReferenceAndListParam theIdentifier) { return null; } @@ -918,8 +871,8 @@ public class ServerConformanceProviderDstu2Test { @Search(type = Patient.class) public Patient findPatient1( @Description(shortDefinition = "The organization at which this person is a patient") - @RequiredParam(name = "organization.foo") ReferenceAndListParam theFoo, - @RequiredParam(name = "organization.bar") ReferenceAndListParam theBar, + @RequiredParam(name = "organization.foo") ReferenceAndListParam theFoo, + @RequiredParam(name = "organization.bar") ReferenceAndListParam theBar, @RequiredParam(name = "organization.baz.bob") ReferenceAndListParam theBazbob) { return null; } @@ -966,10 +919,9 @@ public class ServerConformanceProviderDstu2Test { } - private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); - retVal.setServer(theServer); - return retVal; + @AfterAll + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/InterceptorUserDataMapDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/InterceptorUserDataMapDstu2Test.java index 6f46fa68e66..d543ecfa2d1 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/InterceptorUserDataMapDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/InterceptorUserDataMapDstu2Test.java @@ -42,6 +42,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -78,7 +79,7 @@ public class InterceptorUserDataMapDstu2Test { @BeforeEach public void beforePurgeMap() { myMap = null; - myMapCheckMethods = new LinkedHashSet<>(); + myMapCheckMethods = Collections.synchronizedSet(new LinkedHashSet<>()); } diff --git a/hapi-fhir-structures-dstu3/pom.xml b/hapi-fhir-structures-dstu3/pom.xml index 5a825a62517..f9d795a9068 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java index c01caf7a9db..1deaa202fc2 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProvider.java @@ -318,7 +318,7 @@ public class ServerCapabilityStatementProvider extends BaseServerCapabilityState } } else if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { // Only add each operation (by name) once rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition(new Reference("OperationDefinition/" + opName)); @@ -353,7 +353,7 @@ public class ServerCapabilityStatementProvider extends BaseServerCapabilityState checkBindingForSystemOps(rest, systemOps, nextMethodBinding); if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { ourLog.debug("Found bound operation: {}", opName); rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition(new Reference("OperationDefinition/" + opName)); @@ -466,7 +466,7 @@ public class ServerCapabilityStatementProvider extends BaseServerCapabilityState RestfulServerConfiguration serverConfiguration = getServerConfiguration(theRequestDetails); Bindings bindings = serverConfiguration.provideBindings(); - List operationBindings = bindings.getOperationNameToBindings().get(theId.getIdPart()); + List operationBindings = bindings.getOperationIdToBindings().get(theId.getIdPart()); if (operationBindings != null && !operationBindings.isEmpty()) { return readOperationDefinitionForOperation(operationBindings); } @@ -572,10 +572,10 @@ public class ServerCapabilityStatementProvider extends BaseServerCapabilityState for (IParameter nextParamUntyped : sharedDescription.getParameters()) { if (nextParamUntyped instanceof OperationParameter) { OperationParameter nextParam = (OperationParameter) nextParamUntyped; - OperationDefinitionParameterComponent param = op.addParameter(); if (!inParams.add(nextParam.getName())) { continue; } + OperationDefinitionParameterComponent param = op.addParameter(); param.setUse(OperationParameterUse.IN); if (nextParam.getParamType() != null) { param.setType(nextParam.getParamType()); diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu3Test.java index 63c2f2befd8..b5382da1516 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu3Test.java @@ -102,7 +102,7 @@ public class OperationServerDstu3Test { */ @Test public void testOperationDefinition() { - OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient--OP_TYPE").execute(); + OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient-t-OP_TYPE").execute(); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(def)); @@ -565,14 +565,6 @@ public class OperationServerDstu3Test { } - public static void main(String[] theValue) { - Parameters p = new Parameters(); - p.addParameter().setName("start").setValue(new DateTimeType("2001-01-02")); - p.addParameter().setName("end").setValue(new DateTimeType("2015-07-10")); - String inParamsStr = FhirContext.forDstu2().newXmlParser().encodeResourceToString(p); - ourLog.info(inParamsStr.replace("\"", "\\\"")); - } - public static class PatientProvider implements IResourceProvider { @Override diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu3Test.java index 6393450147a..3842f6ba77c 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerWithSearchParamTypesDstu3Test.java @@ -193,7 +193,7 @@ public class OperationServerWithSearchParamTypesDstu3Test { /* * Check the operation definitions themselves */ - OperationDefinition andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--andlist"), createRequestDetails(rs)); + OperationDefinition andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-andlist"), createRequestDetails(rs)); String def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(andListDef); ourLog.info(def); //@formatter:off @@ -209,7 +209,7 @@ public class OperationServerWithSearchParamTypesDstu3Test { )); //@formatter:on - andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--andlist-withnomax"), createRequestDetails(rs)); + andListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-andlist-withnomax"), createRequestDetails(rs)); def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(andListDef); ourLog.info(def); //@formatter:off @@ -225,7 +225,7 @@ public class OperationServerWithSearchParamTypesDstu3Test { )); //@formatter:on - OperationDefinition orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--orlist"), createRequestDetails(rs)); + OperationDefinition orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-orlist"), createRequestDetails(rs)); def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(orListDef); ourLog.info(def); //@formatter:off @@ -241,7 +241,7 @@ public class OperationServerWithSearchParamTypesDstu3Test { )); //@formatter:on - orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient--orlist-withnomax"), createRequestDetails(rs)); + orListDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-t-orlist-withnomax"), createRequestDetails(rs)); def = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(orListDef); ourLog.info(def); //@formatter:off @@ -490,7 +490,8 @@ public class OperationServerWithSearchParamTypesDstu3Test { @Operation(name = "$orlist-withnomax", idempotent = true) public Parameters orlistWithNoMax( //@formatter:off - @OperationParam(name="valstr") List theValStr, + @OperationParam(name="valstr" + ) List theValStr, @OperationParam(name="valtok") List theValTok //@formatter:on ) { @@ -504,7 +505,7 @@ public class OperationServerWithSearchParamTypesDstu3Test { } private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); + ServletRequestDetails retVal = new ServletRequestDetails(); retVal.setServer(theServer); return retVal; } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/XmlUtilDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/XmlUtilDstu3Test.java index 6cdee55aaf6..92cea006890 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/XmlUtilDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/util/XmlUtilDstu3Test.java @@ -81,9 +81,11 @@ public class XmlUtilDstu3Test { public void testEncodePrettyPrint() throws IOException, SAXException, TransformerException { String input = ""; Document parsed = XmlUtil.parseDocument(input); - String output = XmlUtil.encodeDocument(parsed, true); + String output = XmlUtil.encodeDocument(parsed, true) + .replace("\r\n", "\n") + .replaceAll("^ *", ""); assertEquals("\n" + - " \n" + + "\n" + "\n", output); } diff --git a/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java index 7de10333d3f..2074d19d5f2 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerCapabilityStatementProviderDstu3Test.java @@ -338,55 +338,26 @@ public class ServerCapabilityStatementProviderDstu3Test { String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - assertEquals(4, conformance.getRest().get(0).getOperation().size()); + assertEquals(2, conformance.getRest().get(0).getOperation().size()); List operationNames = toOperationNames(conformance.getRest().get(0).getOperation()); - assertThat(operationNames, containsInAnyOrder("someOp", "validate", "someOp", "validate")); + assertThat(operationNames, containsInAnyOrder("someOp", "validate")); List operationIdParts = toOperationIdParts(conformance.getRest().get(0).getOperation()); - assertThat(operationIdParts, containsInAnyOrder("Patient-i-someOp", "Encounter-i-someOp", "Patient-i-validate", "Encounter-i-validate")); + assertThat(operationIdParts, containsInAnyOrder("EncounterPatient-i-someOp", "EncounterPatient-i-validate")); { - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); + OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/EncounterPatient-i-someOp"), createRequestDetails(rs)); validate(opDef); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); Set types = toStrings(opDef.getResource()); assertEquals("someOp", opDef.getCode()); assertEquals(true, opDef.getInstance()); assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); assertEquals(2, opDef.getParameter().size()); assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); assertEquals("date", opDef.getParameter().get(0).getType()); assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Patient", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set types = toStrings(opDef.getResource()); - assertEquals("someOp", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Encounter")); - assertEquals(2, opDef.getParameter().size()); - assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); - assertEquals("date", opDef.getParameter().get(0).getType()); - assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Encounter", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set types = toStrings(opDef.getResource()); - assertEquals("validate", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); - assertEquals(1, opDef.getParameter().size()); - assertEquals("resource", opDef.getParameter().get(0).getName()); - assertEquals("Patient", opDef.getParameter().get(0).getType()); } } @@ -410,45 +381,6 @@ public class ServerCapabilityStatementProviderDstu3Test { } - @Test - public void testOperationOnNoTypes() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); - rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider() { - @Override - public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - return super.getServerConformance(theRequest, createRequestDetails(rs)); - } - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); - validate(opDef); - - assertEquals("plain", opDef.getCode()); - assertEquals(true, opDef.getIdempotent()); - assertEquals(3, opDef.getParameter().size()); - - assertTrue(opDef.getParameter().get(0).hasName()); - assertEquals("start", opDef.getParameter().get(0).getName()); - assertEquals("in", opDef.getParameter().get(0).getUse().toCode()); - assertEquals("0", opDef.getParameter().get(0).getMinElement().getValueAsString()); - assertEquals("date", opDef.getParameter().get(0).getTypeElement().getValueAsString()); - - assertEquals("out1", opDef.getParameter().get(2).getName()); - assertEquals("out", opDef.getParameter().get(2).getUse().toCode()); - assertEquals("1", opDef.getParameter().get(2).getMinElement().getValueAsString()); - assertEquals("2", opDef.getParameter().get(2).getMaxElement().getValueAsString()); - assertEquals("string", opDef.getParameter().get(2).getTypeElement().getValueAsString()); - - assertThat(opDef.getSystem(), is(true)); - assertThat(opDef.getType(), is(false)); - assertThat(opDef.getInstance(), is(true)); - } - @Test public void testProviderWithRequiredAndOptional() throws Exception { @@ -726,6 +658,7 @@ public class ServerCapabilityStatementProviderDstu3Test { } @Test + @Disabled // This was working incorrectly previously public void testSystemLevelNamedQueryWithParameters() throws Exception { RestfulServer rs = new RestfulServer(ourCtx); rs.setProviders(new NamedQueryPlainProvider()); @@ -1272,7 +1205,7 @@ public class ServerCapabilityStatementProviderDstu3Test { public static class PatientTripleSub extends PatientSubSub {} private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); + ServletRequestDetails retVal = new ServletRequestDetails(); retVal.setServer(theServer); return retVal; } diff --git a/hapi-fhir-structures-hl7org-dstu2/pom.xml b/hapi-fhir-structures-hl7org-dstu2/pom.xml index e842ad1c049..5f1386f1496 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/dstu2/hapi/rest/server/ServerConformanceProvider.java b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/dstu2/hapi/rest/server/ServerConformanceProvider.java index c68ff5fd5f6..89108b0b695 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/dstu2/hapi/rest/server/ServerConformanceProvider.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/main/java/org/hl7/fhir/dstu2/hapi/rest/server/ServerConformanceProvider.java @@ -263,7 +263,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv (SearchMethodBinding) nextMethodBinding, theRequestDetails); } else if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { // Only add each operation (by name) once rest.addOperation().setName(methodBinding.getName()).getDefinition() @@ -299,7 +299,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv checkBindingForSystemOps(rest, systemOps, nextMethodBinding); if (nextMethodBinding instanceof OperationMethodBinding) { OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; - String opName = bindings.getOperationBindingToName().get(methodBinding); + String opName = bindings.getOperationBindingToId().get(methodBinding); if (operationNames.add(opName)) { rest.addOperation().setName(methodBinding.getName()).getDefinition() .setReference("OperationDefinition/" + opName); @@ -412,7 +412,7 @@ public class ServerConformanceProvider extends BaseServerCapabilityStatementProv if (theId == null || theId.hasIdPart() == false) { throw new ResourceNotFoundException(theId); } - List sharedDescriptions = getServerConfiguration(theRequestDetails).provideBindings().getOperationNameToBindings().get(theId.getIdPart()); + List sharedDescriptions = getServerConfiguration(theRequestDetails).provideBindings().getOperationIdToBindings().get(theId.getIdPart()); if (sharedDescriptions == null || sharedDescriptions.isEmpty()) { throw new ResourceNotFoundException(theId); } diff --git a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerHl7OrgDstu2Test.java b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerHl7OrgDstu2Test.java index 84a10a258f5..7aabf4fc2fa 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerHl7OrgDstu2Test.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationDuplicateServerHl7OrgDstu2Test.java @@ -56,14 +56,14 @@ public class OperationDuplicateServerHl7OrgDstu2Test { ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(resp)); - assertEquals(3, resp.getRest().get(0).getOperation().size()); + assertEquals(1, resp.getRest().get(0).getOperation().size()); assertEquals("$myoperation", resp.getRest().get(0).getOperation().get(0).getName()); - assertEquals("OperationDefinition/-s-myoperation", resp.getRest().get(0).getOperation().get(0).getDefinition().getReference()); + assertEquals("OperationDefinition/OrganizationPatient-ts-myoperation", resp.getRest().get(0).getOperation().get(0).getDefinition().getReference()); } // OperationDefinition { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/OperationDefinition/Patient--myoperation?_pretty=true"); + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/OperationDefinition/OrganizationPatient-ts-myoperation?_pretty=true"); HttpResponse status = ourClient.execute(httpGet); assertEquals(200, status.getStatusLine().getStatusCode()); @@ -74,7 +74,7 @@ public class OperationDuplicateServerHl7OrgDstu2Test { OperationDefinition resp = ourCtx.newXmlParser().parseResource(OperationDefinition.class, response); assertEquals("$myoperation", resp.getCode()); assertEquals(true, resp.getIdempotent()); - assertEquals(1, resp.getType().size()); + assertEquals(2, resp.getType().size()); assertEquals(1, resp.getParameter().size()); } } diff --git a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderHl7OrgDstu2Test.java b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderHl7OrgDstu2Test.java index fccaab41961..7108cb5fe1d 100644 --- a/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderHl7OrgDstu2Test.java +++ b/hapi-fhir-structures-hl7org-dstu2/src/test/java/ca/uhn/fhir/rest/server/ServerConformanceProviderHl7OrgDstu2Test.java @@ -243,44 +243,6 @@ public class ServerConformanceProviderHl7OrgDstu2Test { } - @Test - public void testOperationOnNoTypes() throws Exception { - RestfulServer rs = new RestfulServer(ourCtx); - rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); - - ServerConformanceProvider sc = new ServerConformanceProvider(rs) { - @Override - public Conformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - return super.getServerConformance(theRequest, theRequestDetails); - } - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - Conformance sconf = sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - assertEquals("OperationDefinition/-is-plain", sconf.getRest().get(0).getOperation().get(0).getDefinition().getReference()); - - OperationDefinition opDef = sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); - - String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef); - ourLog.info(conf); - - assertEquals("$plain", opDef.getCode()); - assertEquals(true, opDef.getIdempotent()); - assertEquals(3, opDef.getParameter().size()); - assertEquals("start", opDef.getParameter().get(0).getName()); - assertEquals("in", opDef.getParameter().get(0).getUse().toCode()); - assertEquals("0", opDef.getParameter().get(0).getMinElement().getValueAsString()); - assertEquals("date", opDef.getParameter().get(0).getTypeElement().getValueAsString()); - - assertEquals("out1", opDef.getParameter().get(2).getName()); - assertEquals("out", opDef.getParameter().get(2).getUse().toCode()); - assertEquals("1", opDef.getParameter().get(2).getMinElement().getValueAsString()); - assertEquals("2", opDef.getParameter().get(2).getMaxElement().getValueAsString()); - assertEquals("string", opDef.getParameter().get(2).getTypeElement().getValueAsString()); -} - @Test public void testProviderWithRequiredAndOptional() throws Exception { @@ -617,7 +579,7 @@ public class ServerConformanceProviderHl7OrgDstu2Test { } private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); + ServletRequestDetails retVal = new ServletRequestDetails(); retVal.setServer(theServer); return retVal; } diff --git a/hapi-fhir-structures-r4/pom.xml b/hapi-fhir-structures-r4/pom.xml index d5f9e85f836..6bdf2b268dd 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java index 8ee22ee169b..84cfb5b1038 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/OperationServerR4Test.java @@ -33,8 +33,18 @@ import org.eclipse.jetty.servlet.ServletHolder; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; -import org.hl7.fhir.r4.model.*; +import org.hl7.fhir.r4.model.Binary; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CapabilityStatement; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.IntegerType; +import org.hl7.fhir.r4.model.MoneyQuantity; +import org.hl7.fhir.r4.model.OperationDefinition; import org.hl7.fhir.r4.model.OperationDefinition.OperationParameterUse; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.Patient; +import org.hl7.fhir.r4.model.StringType; +import org.hl7.fhir.r4.model.UnsignedIntType; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -72,6 +82,7 @@ public class OperationServerR4Test { private static int ourPort; private static Server ourServer; private static IBaseResource ourNextResponse; + private static RestOperationTypeEnum ourLastRestOperation; private IGenericClient myFhirClient; @BeforeEach @@ -98,11 +109,11 @@ public class OperationServerR4Test { CapabilityStatement p = myFhirClient.fetchConformance().ofType(CapabilityStatement.class).prettyPrint().execute(); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(p)); - List ops = p.getRest().get(0).getOperation(); + List ops = p.getRestFirstRep().getResource().stream().filter(t->t.getType().equals("Patient")).findFirst().orElseThrow(()->new IllegalArgumentException()).getOperation(); assertThat(ops.size(), greaterThan(1)); List opNames = toOpNames(ops); - assertThat(opNames, containsInRelativeOrder("OP_TYPE")); + assertThat(opNames.toString(), opNames, containsInRelativeOrder("OP_TYPE")); OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId(ops.get(opNames.indexOf("OP_TYPE")).getDefinition()).execute(); assertEquals("OP_TYPE", def.getCode()); @@ -113,7 +124,7 @@ public class OperationServerR4Test { */ @Test public void testOperationDefinition() { - OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient--OP_TYPE").execute(); + OperationDefinition def = myFhirClient.read().resource(OperationDefinition.class).withId("OperationDefinition/Patient-t-OP_TYPE").execute(); ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(def)); @@ -179,8 +190,6 @@ public class OperationServerR4Test { } - - @Test public void testManualResponseWithPrimitiveParam() throws Exception { @@ -197,7 +206,6 @@ public class OperationServerR4Test { } - @Test public void testInstanceEverythingGet() throws Exception { @@ -230,7 +238,6 @@ public class OperationServerR4Test { assertEquals(RestOperationTypeEnum.EXTENDED_OPERATION_INSTANCE, ourLastRestOperation); } - @Test public void testInstanceEverythingHapiClient() { ourCtx.newRestfulGenericClient("http://localhost:" + ourPort).operation().onInstance(new IdType("Patient/123")).named("$everything").withParameters(new Parameters()).execute(); @@ -278,7 +285,7 @@ public class OperationServerR4Test { @Test public void testManualInputAndOutput() throws Exception { - byte[] bytes = new byte[]{1,2,3,4,5,6,7,8,7,6,5,4,3,2,1}; + byte[] bytes = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1}; ContentType contentType = ContentType.IMAGE_PNG; HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$manualInputAndOutput"); @@ -295,10 +302,9 @@ public class OperationServerR4Test { } - @Test public void testManualInputAndOutputWithUrlParam() throws Exception { - byte[] bytes = new byte[]{1,2,3,4,5,6,7,8,7,6,5,4,3,2,1}; + byte[] bytes = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1}; ContentType contentType = ContentType.IMAGE_PNG; HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$manualInputAndOutputWithParam?param1=value"); @@ -838,7 +844,7 @@ public class OperationServerR4Test { return new Bundle(); } - @Operation(name="$manualInputAndOutput", manualResponse=true, manualRequest=true) + @Operation(name = "$manualInputAndOutput", manualResponse = true, manualRequest = true) public void manualInputAndOutput(HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws IOException { String contentType = theServletRequest.getContentType(); byte[] bytes = IOUtils.toByteArray(theServletRequest.getInputStream()); @@ -850,9 +856,9 @@ public class OperationServerR4Test { theServletResponse.getOutputStream().close(); } - @Operation(name="$manualInputAndOutputWithParam", manualResponse=true, manualRequest=true) + @Operation(name = "$manualInputAndOutputWithParam", manualResponse = true, manualRequest = true) public void manualInputAndOutputWithParam( - @OperationParam(name="param1") StringType theParam1, + @OperationParam(name = "param1") StringType theParam1, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse ) throws IOException { @@ -880,7 +886,6 @@ public class OperationServerR4Test { } } - private static RestOperationTypeEnum ourLastRestOperation; public static class PlainProvider { @@ -918,10 +923,10 @@ public class OperationServerR4Test { return new SimpleBundleProvider(resources); } - @Operation(name= "$manualResponseWithPrimitiveParam", idempotent = true, global = true, manualResponse = true) + @Operation(name = "$manualResponseWithPrimitiveParam", idempotent = true, global = true, manualResponse = true) public void manualResponseWithPrimitiveParam( @IdParam IIdType theResourceId, - @OperationParam(name="path", min = 1, max = 1) IPrimitiveType thePath, + @OperationParam(name = "path", min = 1, max = 1) IPrimitiveType thePath, ServletRequestDetails theRequestDetails, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) { @@ -933,7 +938,7 @@ public class OperationServerR4Test { theServletResponse.setStatus(200); } - @Operation(name = "$OP_SERVER") + @Operation(name = "$OP_SERVER") public Parameters opServer( @OperationParam(name = "PARAM1") StringType theParam1, @OperationParam(name = "PARAM2") Patient theParam2 @@ -1028,12 +1033,4 @@ public class OperationServerR4Test { } - public static void main(String[] theValue) { - Parameters p = new Parameters(); - p.addParameter().setName("start").setValue(new DateTimeType("2001-01-02")); - p.addParameter().setName("end").setValue(new DateTimeType("2015-07-10")); - String inParamsStr = FhirContext.forDstu2().newXmlParser().encodeResourceToString(p); - ourLog.info(inParamsStr.replace("\"", "\\\"")); - } - } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulServerTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/RestfulServerTest.java similarity index 67% rename from hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulServerTest.java rename to hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/RestfulServerTest.java index 95c67bec9bb..679c1ec3a6f 100644 --- a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulServerTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/RestfulServerTest.java @@ -3,7 +3,7 @@ package ca.uhn.fhir.rest.server; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.model.api.annotation.ResourceDef; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.Metadata; import ca.uhn.fhir.rest.annotation.Operation; @@ -16,11 +16,10 @@ import org.hl7.fhir.instance.model.api.IBaseConformance; import org.hl7.fhir.instance.model.api.IBaseMetaType; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Patient; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Answers; -import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import javax.servlet.ServletException; @@ -33,70 +32,55 @@ import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class RestfulServerTest { - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private FhirContext myCtx; - private RestfulServer restfulServer; + private final FhirContext myCtx = FhirContext.forR4(); // don't use cached, we register custom resources + private RestfulServer myRestfulServer; @BeforeEach public void setUp() throws ServletException { - when(myCtx.getVersion().getVersion()).thenReturn(FhirVersionEnum.DSTU3); - when(myCtx.getVersion().getServerVersion()).thenReturn(new MyFhirVersionServer()); - - restfulServer = new RestfulServer(myCtx); - restfulServer.init(); - } - - private void mockResource(Class theClass) { - RuntimeResourceDefinition resourceDefinitionMock = mock(RuntimeResourceDefinition.class); - String className = theClass.getSimpleName(); - lenient().when(resourceDefinitionMock.getName()).thenReturn(className); - lenient().when(myCtx.getResourceDefinition(className)).thenReturn(resourceDefinitionMock); - lenient().when(myCtx.getResourceType(theClass)).thenReturn(className); + myRestfulServer = new RestfulServer(myCtx); + myRestfulServer.init(); } @Test public void testRegisterProvidersWithMethodBindings() { - mockResource(MyResource.class); - mockResource(MyResource2.class); - MyProvider provider = new MyProvider(); - restfulServer.registerProvider(provider); + myRestfulServer.registerProvider(provider); MyProvider2 provider2 = new MyProvider2(); - restfulServer.registerProvider(provider2); + myRestfulServer.registerProvider(provider2); - assertFalse(restfulServer.getProviderMethodBindings(provider).isEmpty()); - assertFalse(restfulServer.getProviderMethodBindings(provider2).isEmpty()); + assertFalse(myRestfulServer.getProviderMethodBindings(provider).isEmpty()); + assertFalse(myRestfulServer.getProviderMethodBindings(provider2).isEmpty()); - restfulServer.unregisterProvider(provider); - assertTrue(restfulServer.getProviderMethodBindings(provider).isEmpty()); - assertFalse(restfulServer.getProviderMethodBindings(provider2).isEmpty()); + myRestfulServer.unregisterProvider(provider); + assertTrue(myRestfulServer.getProviderMethodBindings(provider).isEmpty()); + assertFalse(myRestfulServer.getProviderMethodBindings(provider2).isEmpty()); } @Test public void testRegisterProviders() { //test register Plain Provider - restfulServer.registerProvider(new MyClassWithRestInterface()); - assertEquals(1, restfulServer.getPlainProviders().size()); - Object plainProvider = restfulServer.getPlainProviders().iterator().next(); + myRestfulServer.registerProvider(new MyClassWithRestInterface()); + assertEquals(1, myRestfulServer.getResourceProviders().size()); + Object plainProvider = myRestfulServer.getResourceProviders().get(0); assertTrue(plainProvider instanceof MyClassWithRestInterface); //test register Resource Provider - restfulServer.registerProvider(new MyResourceProvider()); - assertEquals(1, restfulServer.getResourceProviders().size()); - IResourceProvider resourceProvider = restfulServer.getResourceProviders().iterator().next(); + myRestfulServer.registerProvider(new MyResourceProvider()); + assertEquals(2, myRestfulServer.getResourceProviders().size()); + IResourceProvider resourceProvider = myRestfulServer.getResourceProviders().get(1); assertTrue(resourceProvider instanceof MyResourceProvider); //test unregister providers - restfulServer.unregisterProvider(plainProvider); - assertTrue(restfulServer.getPlainProviders().isEmpty()); - restfulServer.unregisterProvider(resourceProvider); - assertTrue(restfulServer.getResourceProviders().isEmpty()); + myRestfulServer.unregisterProvider(plainProvider); + assertFalse(myRestfulServer.getResourceProviders().isEmpty()); + myRestfulServer.unregisterProvider(resourceProvider); + assertTrue(myRestfulServer.getResourceProviders().isEmpty()); } @Test public void testFailRegisterInterfaceProviderWithoutRestfulMethod() { try { - restfulServer.registerProvider(new MyClassWithoutRestInterface()); + myRestfulServer.registerProvider(new MyClassWithoutRestInterface()); fail(); } catch (ConfigurationException e) { assertEquals("Did not find any annotated RESTful methods on provider class ca.uhn.fhir.rest.server.RestfulServerTest$MyClassWithoutRestInterface", e.getMessage()); @@ -108,7 +92,11 @@ public class RestfulServerTest { private static class MyClassWithoutRestInterface implements Serializable { } - private static class MyClassWithRestInterface implements MyRestInterface { + private static class MyClassWithRestInterface implements MyRestInterface, IResourceProvider { + @Override + public Class getResourceType() { + return Patient.class; + } } @SuppressWarnings("unused") @@ -148,7 +136,7 @@ public class RestfulServerTest { @Override public Class getResourceType() { - return IBaseResource.class; + return Patient.class; } } @@ -176,7 +164,8 @@ public class RestfulServerTest { } } - private static class MyResource implements IBaseResource { + @ResourceDef(name="MyResource") + public static class MyResource implements IBaseResource { @Override public boolean isEmpty() { @@ -230,11 +219,12 @@ public class RestfulServerTest { @Override public FhirVersionEnum getStructureFhirVersionEnum() { - return null; + return FhirVersionEnum.R4; } } - private static class MyResource2 extends MyResource { + @ResourceDef(name="MyResource2") + public static class MyResource2 extends MyResource { } } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerInvalidDefinitionR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerInvalidDefinitionR4Test.java index 57e485b792a..eac229a77c8 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerInvalidDefinitionR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/ServerInvalidDefinitionR4Test.java @@ -2,7 +2,9 @@ package ca.uhn.fhir.rest.server; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; +import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.Operation; import ca.uhn.fhir.rest.annotation.OperationParam; import ca.uhn.fhir.rest.annotation.OptionalParam; @@ -11,24 +13,35 @@ import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.server.IBundleProvider; +import ca.uhn.fhir.rest.api.server.RequestDetails; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider; +import ca.uhn.fhir.test.utilities.server.MockServletUtil; import ca.uhn.fhir.util.TestUtil; import com.google.common.collect.Lists; import org.hamcrest.core.StringContains; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.CapabilityStatement; +import org.hl7.fhir.r4.model.DateType; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.OperationDefinition; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; import java.util.List; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; public class ServerInvalidDefinitionR4Test extends BaseR4ServerTest { @@ -182,9 +195,31 @@ public class ServerInvalidDefinitionR4Test extends BaseR4ServerTest { } - @AfterAll - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); + @Test + public void testOperationOnNoTypes() throws Exception { + @SuppressWarnings("unused") + class PlainProviderWithExtendedOperationOnNoType { + + @Operation(name = "plain", idempotent = true, returnParameters = {@OperationParam(min = 1, max = 2, name = "out1", type = StringType.class)}) + public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, + @OperationParam(name = "end") DateType theEnd) { + return null; + } + + } + + RestfulServer rs = new RestfulServer(FhirContext.forCached(FhirVersionEnum.R4)); + rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + try { + rs.init(MockServletUtil.createServletConfig()); + fail(); + } catch (ServletException e) { + assertEquals("Failed to initialize FHIR Restful server: Failure scanning class PlainProviderWithExtendedOperationOnNoType: @Operation method is an instance level method (it has an @IdParam parameter) but is not marked as global() and is not declared in a resource provider: everything", e.getMessage()); + } + } + } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java index 480568284fb..3bb305e4211 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorR4Test.java @@ -302,8 +302,37 @@ public class AuthorizationInterceptorR4Test { assertEquals(200, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$validate"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); } + + /** + * A GET to the base URL isn't valid, but the interceptor should allow it + */ + @Test + public void testGetRoot() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + return new RuleBuilder() + .allowAll() + .build(); + } + }); + + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/"); + CloseableHttpResponse status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(400, status.getStatusLine().getStatusCode()); + + } + + @Test public void testAllowAllForTenant() throws Exception { ourServlet.setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy()); @@ -1944,7 +1973,6 @@ public class AuthorizationInterceptorR4Test { HttpGet httpGet; HttpResponse status; - String response; ourReturn = Collections.singletonList(createPatient(2)); ourHitMethod = false; @@ -1968,7 +1996,6 @@ public class AuthorizationInterceptorR4Test { HttpGet httpGet; HttpResponse status; - String response; ourReturn = Collections.singletonList(createPatient(2)); ourHitMethod = false; @@ -2188,7 +2215,6 @@ public class AuthorizationInterceptorR4Test { HttpGet httpGet; HttpResponse status; - String response; ourReturn = Collections.singletonList(new Consent().setDateTime(new Date()).setId("Consent/123")); ourHitMethod = false; @@ -2933,7 +2959,7 @@ public class AuthorizationInterceptorR4Test { HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/"); httpPost.setEntity(createFhirResourceEntity(requestBundle)); CloseableHttpResponse status = ourClient.execute(httpPost); - String resp = extractResponseAndClose(status); + extractResponseAndClose(status); assertEquals(200, status.getStatusLine().getStatusCode()); } diff --git a/hapi-fhir-structures-r5/pom.xml b/hapi-fhir-structures-r5/pom.xml index 490ba126870..3a1dbc48cb6 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-structures-r5/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR5Test.java b/hapi-fhir-structures-r5/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR5Test.java index 19f0ca0dbe7..d1940e3649f 100644 --- a/hapi-fhir-structures-r5/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR5Test.java +++ b/hapi-fhir-structures-r5/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR5Test.java @@ -125,7 +125,7 @@ public class ServerCapabilityStatementProviderR5Test { } private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); + ServletRequestDetails retVal = new ServletRequestDetails(); retVal.setServer(theServer); return retVal; } @@ -147,8 +147,9 @@ public class ServerCapabilityStatementProviderR5Test { String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - assertEquals(1, conformance.getRest().get(0).getOperation().size()); - assertEquals("everything", conformance.getRest().get(0).getOperation().get(0).getName()); + List operations = conformance.getRestFirstRep().getResource().stream().filter(t->t.getType().equals("Patient")).findFirst().orElseThrow(()->new IllegalArgumentException()).getOperation(); + assertEquals(1, operations.size()); + assertEquals("everything", operations.get(0).getName()); OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); validate(opDef); @@ -163,6 +164,7 @@ public class ServerCapabilityStatementProviderR5Test { RestfulServer rs = new RestfulServer(myCtx); rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { }; @@ -291,56 +293,42 @@ public class ServerCapabilityStatementProviderR5Test { String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); + List operations; - assertEquals(4, conformance.getRest().get(0).getOperation().size()); - List operationNames = toOperationNames(conformance.getRest().get(0).getOperation()); - assertThat(operationNames, containsInAnyOrder("someOp", "validate", "someOp", "validate")); - - List operationIdParts = toOperationIdParts(conformance.getRest().get(0).getOperation()); - assertThat(operationIdParts, containsInAnyOrder("Patient-i-someOp", "Encounter-i-someOp", "Patient-i-validate", "Encounter-i-validate")); + operations = conformance.getRestFirstRep().getResource().stream().filter(t->t.getType().equals("Patient")).findFirst().orElseThrow(()->new IllegalArgumentException()).getOperation(); + assertEquals(2, operations.size()); + List operationNames = toOperationNames(operations); + assertThat(operationNames.toString(), operationNames, containsInAnyOrder("someOp", "validate")); + List operationIdParts = toOperationIdParts(operations); + assertThat(operationIdParts.toString(), operationIdParts, containsInAnyOrder("EncounterPatient-i-someOp", "EncounterPatient-i-validate")); { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/EncounterPatient-i-someOp"), createRequestDetails(rs)); validate(opDef); ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); Set types = toStrings(opDef.getResource()); assertEquals("someOp", opDef.getCode()); assertEquals(true, opDef.getInstance()); assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); assertEquals(2, opDef.getParameter().size()); assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); assertEquals("date", opDef.getParameter().get(0).getType().toCode()); assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Patient", opDef.getParameter().get(1).getType().toCode()); + assertEquals("Resource", opDef.getParameter().get(1).getType().toCode()); } { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set types = toStrings(opDef.getResource()); - assertEquals("someOp", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Encounter")); - assertEquals(2, opDef.getParameter().size()); - assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); - assertEquals("date", opDef.getParameter().get(0).getType().toCode()); - assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Encounter", opDef.getParameter().get(1).getType().toCode()); - } - { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/EncounterPatient-i-validate"), createRequestDetails(rs)); validate(opDef); ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); Set types = toStrings(opDef.getResource()); assertEquals("validate", opDef.getCode()); assertEquals(true, opDef.getInstance()); assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); assertEquals(1, opDef.getParameter().size()); assertEquals("resource", opDef.getParameter().get(0).getName()); - assertEquals("Patient", opDef.getParameter().get(0).getType().toCode()); + assertEquals("Resource", opDef.getParameter().get(0).getType().toCode()); } } @@ -364,45 +352,6 @@ public class ServerCapabilityStatementProviderR5Test { } - @Test - public void testOperationOnNoTypes() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { - @Override - public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - return (CapabilityStatement) super.getServerConformance(theRequest, createRequestDetails(rs)); - } - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); - validate(opDef); - - assertEquals("plain", opDef.getCode()); - assertEquals(false, opDef.getAffectsState()); - assertEquals(3, opDef.getParameter().size()); - - assertTrue(opDef.getParameter().get(0).hasName()); - assertEquals("start", opDef.getParameter().get(0).getName()); - assertEquals("in", opDef.getParameter().get(0).getUse().toCode()); - assertEquals("0", opDef.getParameter().get(0).getMinElement().getValueAsString()); - assertEquals("date", opDef.getParameter().get(0).getTypeElement().getValueAsString()); - - assertEquals("out1", opDef.getParameter().get(2).getName()); - assertEquals("out", opDef.getParameter().get(2).getUse().toCode()); - assertEquals("1", opDef.getParameter().get(2).getMinElement().getValueAsString()); - assertEquals("2", opDef.getParameter().get(2).getMaxElement().getValueAsString()); - assertEquals("string", opDef.getParameter().get(2).getTypeElement().getValueAsString()); - - assertThat(opDef.getSystem(), is(true)); - assertThat(opDef.getType(), is(false)); - assertThat(opDef.getInstance(), is(true)); - } - @Test public void testProviderWithRequiredAndOptional() throws Exception { @@ -706,7 +655,7 @@ public class ServerCapabilityStatementProviderR5Test { ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); validate(operationDefinition); assertThat(operationDefinition.getCode(), is(NamedQueryPlainProvider.QUERY_NAME)); - assertThat("The operation name should be the description, if a description is set", operationDefinition.getName(), equalTo("Search_testQuery")); + assertThat("The operation name should be the description, if a description is set", operationDefinition.getName(), equalTo("TestQuery")); assertThat(operationDefinition.getStatus(), is(PublicationStatus.ACTIVE)); assertThat(operationDefinition.getKind(), is(OperationKind.QUERY)); assertThat(operationDefinition.getDescription(), is(NamedQueryPlainProvider.DESCRIPTION)); @@ -740,15 +689,15 @@ public class ServerCapabilityStatementProviderR5Test { CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); - CapabilityStatementRestComponent restComponent = conformance.getRest().get(0); - CapabilityStatementRestResourceOperationComponent operationComponent = restComponent.getOperation().get(0); + CapabilityStatementRestResourceComponent resource = conformance.getRestFirstRep().getResource().stream().filter(t->t.getType().equals("Patient")).findFirst().orElseThrow(()->new IllegalArgumentException()); + CapabilityStatementRestResourceOperationComponent operationComponent = resource.getOperation().get(0); String operationReference = operationComponent.getDefinition(); assertThat(operationReference, not(nullValue())); OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); validate(operationDefinition); - assertThat("The operation name should be the code if no description is set", operationDefinition.getName(), equalTo("Search_testQuery")); + assertThat("The operation name should be the code if no description is set", operationDefinition.getName(), equalTo("TestQuery")); String patientResourceName = "Patient"; assertThat("A resource level search targets the resource of the provider it's defined in", operationDefinition.getResource().get(0).getValue(), is(patientResourceName)); assertThat(operationDefinition.getSystem(), is(false)); @@ -764,11 +713,7 @@ public class ServerCapabilityStatementProviderR5Test { assertThat(param.getMax(), is("1")); assertThat(param.getUse(), is(Enumerations.OperationParameterUse.IN)); - CapabilityStatementRestResourceComponent patientResource = restComponent.getResource().stream() - .filter(r -> patientResourceName.equals(r.getType())) - .findAny() - .get(); - assertThat("Named query parameters should not appear in the resource search params", patientResource.getSearchParam(), is(empty())); + assertThat("Named query parameters should not appear in the resource search params", resource.getSearchParam(), is(empty())); } @Test @@ -787,7 +732,7 @@ public class ServerCapabilityStatementProviderR5Test { String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); ourLog.info(conf); - List operations = conformance.getRest().get(0).getOperation(); + List operations = conformance.getRestFirstRep().getResource().stream().filter(t->t.getType().equals("Patient")).findFirst().orElseThrow(()->new IllegalArgumentException()).getOperation(); assertThat(operations.size(), is(1)); assertThat(operations.get(0).getName(), is(TypeLevelOperationProvider.OPERATION_NAME)); diff --git a/hapi-fhir-test-utilities/pom.xml b/hapi-fhir-test-utilities/pom.xml index 66f0f0ac9fd..609c1ab8515 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml @@ -60,7 +60,28 @@ true
    - + + + net.sourceforge.htmlunit + htmlunit + + + xml-apis + xml-apis + + + xerces + xercesImpl + + + + + + org.awaitility + awaitility + + + org.eclipse.jetty jetty-servlet @@ -104,10 +125,18 @@ org.hamcrest hamcrest-core + + junit + junit + + + org.mockito + mockito-core + - + diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/HtmlUtil.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/HtmlUtil.java new file mode 100644 index 00000000000..700b33d9551 --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/HtmlUtil.java @@ -0,0 +1,56 @@ +package ca.uhn.fhir.test.utilities; + +/*- + * #%L + * HAPI FHIR Test Utilities + * %% + * Copyright (C) 2014 - 2021 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 com.gargoylesoftware.htmlunit.BrowserVersion; +import com.gargoylesoftware.htmlunit.StringWebResponse; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlForm; +import com.gargoylesoftware.htmlunit.html.HtmlInput; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.gargoylesoftware.htmlunit.html.parser.neko.HtmlUnitNekoHtmlParser; +import org.awaitility.Awaitility; + +import java.io.IOException; +import java.net.URL; + +public class HtmlUtil { + + public static HtmlPage parseAsHtml(String theRespString, URL theUrl) throws IOException { + StringWebResponse response = new StringWebResponse(theRespString, theUrl); + WebClient client = new WebClient(BrowserVersion.BEST_SUPPORTED, false, null, -1); + client.getOptions().setCssEnabled(false); + client.getOptions().setJavaScriptEnabled(false); + + final HtmlPage page = new HtmlPage(response, client.getCurrentWindow()); + HtmlUnitNekoHtmlParser htmlUnitNekoHtmlParser = new HtmlUnitNekoHtmlParser(); + htmlUnitNekoHtmlParser.parse(response, page, false); + return page; + } + + public static HtmlForm waitForForm(HtmlPage thePage, String theName) { + return Awaitility.await().until(() -> thePage.getFormByName(theName), t -> t != null); + } + + public static HtmlInput waitForInput(HtmlForm theForm, String theName) { + return Awaitility.await().until(() -> theForm.getInputByName(theName), t -> t != null); + } +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/JettyUtil.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/JettyUtil.java index 86d13644749..fae4d26807e 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/JettyUtil.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/JettyUtil.java @@ -28,6 +28,9 @@ import org.eclipse.jetty.server.handler.StatisticsHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + public class JettyUtil { /** @@ -43,18 +46,20 @@ public class JettyUtil { /** * Starts the given Jetty server, and configures it for graceful shutdown */ - public static void startServer(Server server) throws Exception { + public static void startServer(@Nonnull Server theServer) throws Exception { //Needed for graceful shutdown, see https://github.com/eclipse/jetty.project/issues/2076#issuecomment-353717761 - server.insertHandler(new StatisticsHandler()); - server.start(); + theServer.insertHandler(new StatisticsHandler()); + theServer.start(); } /** - * Shut down the given Jetty server, and release held resources. + * Shut down the given Jetty server, and release held resources. */ - public static void closeServer(Server server) throws Exception { - server.stop(); - server.destroy(); + public static void closeServer(@Nullable Server theServer) throws Exception { + if (theServer != null) { + theServer.stop(); + theServer.destroy(); + } } } diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/MockServletUtil.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/MockServletUtil.java new file mode 100644 index 00000000000..52c92c115ea --- /dev/null +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/MockServletUtil.java @@ -0,0 +1,42 @@ +package ca.uhn.fhir.test.utilities.server; + +/*- + * #%L + * HAPI FHIR Test Utilities + * %% + * Copyright (C) 2014 - 2021 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 javax.servlet.ServletConfig; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class MockServletUtil { + + /** + * Non instantiable + */ + private MockServletUtil() { + super(); + } + + public static ServletConfig createServletConfig() { + ServletConfig sc = mock(ServletConfig.class); + when(sc.getServletContext()).thenReturn(null); + return sc; + } +} diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java index e735dc37c7a..4a8b02f9131 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/server/RestfulServerExtension.java @@ -32,6 +32,7 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.junit.jupiter.api.extension.AfterEachCallback; @@ -54,10 +55,11 @@ public class RestfulServerExtension implements BeforeEachCallback, AfterEachCall private FhirVersionEnum myFhirVersion; private Server myServer; private RestfulServer myServlet; - private int myPort; + private int myPort = 0; private CloseableHttpClient myHttpClient; private IGenericClient myFhirClient; private List> myConsumers = new ArrayList<>(); + private String myServletPath = "/*"; /** * Constructor @@ -94,20 +96,21 @@ public class RestfulServerExtension implements BeforeEachCallback, AfterEachCall } private void startServer() throws Exception { - myServer = new Server(0); + myServer = new Server(myPort); - ServletHandler servletHandler = new ServletHandler(); myServlet = new RestfulServer(myFhirContext); myServlet.setDefaultPrettyPrint(true); if (myProviders != null) { myServlet.registerProviders(myProviders); } ServletHolder servletHolder = new ServletHolder(myServlet); - servletHandler.addServletWithMapping(servletHolder, "/*"); myConsumers.forEach(t -> t.accept(myServlet)); - myServer.setHandler(servletHandler); + ServletContextHandler contextHandler = new ServletContextHandler(); + contextHandler.addServlet(servletHolder, myServletPath); + + myServer.setHandler(contextHandler); myServer.start(); myPort = JettyUtil.getPortForStartedServer(myServer); ourLog.info("Server has started on port {}", myPort); @@ -175,4 +178,14 @@ public class RestfulServerExtension implements BeforeEachCallback, AfterEachCall public void shutDownServer() throws Exception { JettyUtil.closeServer(myServer); } + + public RestfulServerExtension withServletPath(String theServletPath) { + myServletPath = theServletPath; + return this; + } + + public RestfulServerExtension withPort(int thePort) { + myPort = thePort; + return this; + } } diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 072a4f9a21c..ee6382cca38 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../pom.xml diff --git a/hapi-fhir-validation-resources-dstu2.1/pom.xml b/hapi-fhir-validation-resources-dstu2.1/pom.xml index f8cc76070d3..3af7e1b74b1 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-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 a69e2ea236d..53941c421b0 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-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 0061ae4ef6e..9570b03358f 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-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 ace6fe58362..188381def8a 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-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 442455129ad..79b31674df1 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/pom.xml b/hapi-fhir-validation/pom.xml index a441397e88e..1d9dcb5397a 100644 --- a/hapi-fhir-validation/pom.xml +++ b/hapi-fhir-validation/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-deployable-pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../hapi-deployable-pom/pom.xml diff --git a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/RequestValidatingInterceptorDstu3Test.java b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/RequestValidatingInterceptorDstu3Test.java index af0ade61f0d..81a3dae5c27 100644 --- a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/RequestValidatingInterceptorDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/RequestValidatingInterceptorDstu3Test.java @@ -19,6 +19,7 @@ import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.IValidationContext; import ca.uhn.fhir.validation.IValidatorModule; import ca.uhn.fhir.validation.ResultSeverityEnum; +import com.ctc.wstx.shaded.msv_core.verifier.jaxp.DocumentBuilderFactoryImpl; import org.apache.commons.io.IOUtils; import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; @@ -45,6 +46,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import javax.xml.parsers.DocumentBuilderFactory; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; diff --git a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java index f151466e3aa..3f503cdbb8a 100644 --- a/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java +++ b/hapi-fhir-validation/src/test/java/ca/uhn/fhir/rest/server/ServerCapabilityStatementProviderR4Test.java @@ -9,6 +9,9 @@ import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.Delete; +import ca.uhn.fhir.rest.annotation.GraphQL; +import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; +import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; import ca.uhn.fhir.rest.annotation.History; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IncludeParam; @@ -23,6 +26,7 @@ import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.annotation.Validate; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.RequestDetails; @@ -38,14 +42,20 @@ import ca.uhn.fhir.rest.server.method.BaseMethodBinding; import ca.uhn.fhir.rest.server.method.IParameter; import ca.uhn.fhir.rest.server.method.SearchMethodBinding; import ca.uhn.fhir.rest.server.method.SearchParameter; +import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider; +import ca.uhn.fhir.rest.server.provider.ProviderConstants; import ca.uhn.fhir.rest.server.provider.ServerCapabilityStatementProvider; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.test.utilities.server.MockServletUtil; +import ca.uhn.fhir.util.HapiExtensions; import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ValidationResult; import com.google.common.collect.Lists; import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; +import org.hl7.fhir.instance.model.api.IBaseParameters; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CapabilityStatement; @@ -60,6 +70,7 @@ import org.hl7.fhir.r4.model.DateType; import org.hl7.fhir.r4.model.DiagnosticReport; import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; +import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.OperationDefinition; @@ -68,12 +79,10 @@ import org.hl7.fhir.r4.model.OperationDefinition.OperationKind; import org.hl7.fhir.r4.model.OperationDefinition.OperationParameterUse; import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Patient; -import org.hl7.fhir.r4.model.StringType; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; @@ -93,6 +102,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; @@ -100,1345 +110,1565 @@ import static org.mockito.Mockito.when; public class ServerCapabilityStatementProviderR4Test { - public static final String PATIENT_SUB = "PatientSub"; - public static final String PATIENT_SUB_SUB = "PatientSubSub"; - public static final String PATIENT_SUB_SUB_2 = "PatientSubSub2"; - public static final String PATIENT_TRIPLE_SUB = "PatientTripleSub"; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProviderR4Test.class); - private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); - private FhirValidator myValidator; - - @BeforeEach - public void before() { - myValidator = myCtx.newValidator(); - myValidator.registerValidatorModule(new FhirInstanceValidator(myCtx)); - } - - private HttpServletRequest createHttpServletRequest() { - HttpServletRequest req = mock(HttpServletRequest.class); - when(req.getRequestURI()).thenReturn("/FhirStorm/fhir/Patient/_search"); - when(req.getServletPath()).thenReturn("/fhir"); - when(req.getRequestURL()).thenReturn(new StringBuffer().append("http://fhirstorm.dyndns.org:8080/FhirStorm/fhir/Patient/_search")); - when(req.getContextPath()).thenReturn("/FhirStorm"); - return req; - } - - private ServletConfig createServletConfig() { - ServletConfig sc = mock(ServletConfig.class); - when(sc.getServletContext()).thenReturn(null); - return sc; - } - - private CapabilityStatementRestResourceComponent findRestResource(CapabilityStatement conformance, String wantResource) throws Exception { - CapabilityStatementRestResourceComponent resource = null; - for (CapabilityStatementRestResourceComponent next : conformance.getRest().get(0).getResource()) { - if (next.getType().equals(wantResource)) { - resource = next; - } - } - if (resource == null) { - throw new Exception("Could not find resource: " + wantResource); - } - return resource; - } - - @Test - public void testFormats() throws ServletException { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ConditionalProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement cs = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - List formats = cs - .getFormat() - .stream() - .map(t -> t.getCode()) - .collect(Collectors.toList()); - assertThat(formats.toString(), formats, containsInAnyOrder( - "application/fhir+xml", - "xml", - "application/fhir+json", - "json", - "application/x-turtle", - "ttl" - )); - } - - - @Test - public void testConditionalOperations() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ConditionalProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); - - assertEquals(2, conformance.getRest().get(0).getResource().size()); - CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); - assertEquals("Patient", res.getType()); - - assertTrue(res.getConditionalCreate()); - assertEquals(ConditionalDeleteStatus.MULTIPLE, res.getConditionalDelete()); - assertTrue(res.getConditionalUpdate()); - } - - private RequestDetails createRequestDetails(RestfulServer theServer) { - ServletRequestDetails retVal = new ServletRequestDetails(null); - retVal.setServer(theServer); - retVal.setFhirServerBase("http://localhost/baseR4"); - return retVal; - } - - @Test - public void testExtendedOperationReturningBundle() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - assertEquals(1, conformance.getRest().get(0).getOperation().size()); - assertEquals("everything", conformance.getRest().get(0).getOperation().get(0).getName()); - - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); - validate(opDef); - assertEquals("everything", opDef.getCode()); - assertThat(opDef.getSystem(), is(false)); - assertThat(opDef.getType(), is(false)); - assertThat(opDef.getInstance(), is(true)); - } - - @Test - public void testExtendedOperationReturningBundleOperation() throws Exception { + public static final String PATIENT_SUB = "PatientSub"; + public static final String PATIENT_SUB_SUB = "PatientSubSub"; + public static final String PATIENT_SUB_SUB_2 = "PatientSubSub2"; + public static final String PATIENT_TRIPLE_SUB = "PatientTripleSub"; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProviderR4Test.class); + private final FhirContext myCtx = FhirContext.forCached(FhirVersionEnum.R4); + private FhirValidator myValidator; + + @BeforeEach + public void before() { + myValidator = myCtx.newValidator(); + myValidator.registerValidatorModule(new FhirInstanceValidator(myCtx)); + } + + private HttpServletRequest createHttpServletRequest() { + HttpServletRequest req = mock(HttpServletRequest.class); + when(req.getRequestURI()).thenReturn("/FhirStorm/fhir/Patient/_search"); + when(req.getServletPath()).thenReturn("/fhir"); + when(req.getRequestURL()).thenReturn(new StringBuffer().append("http://fhirstorm.dyndns.org:8080/FhirStorm/fhir/Patient/_search")); + when(req.getContextPath()).thenReturn("/FhirStorm"); + return req; + } + + private CapabilityStatementRestResourceComponent findRestResource(CapabilityStatement conformance, String wantResource) throws Exception { + CapabilityStatementRestResourceComponent resource = null; + for (CapabilityStatementRestResourceComponent next : conformance.getRest().get(0).getResource()) { + if (next.getType().equals(wantResource)) { + resource = next; + } + } + if (resource == null) { + throw new Exception("Could not find resource: " + wantResource); + } + return resource; + } + + @Test + public void testFormats() throws ServletException { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ConditionalProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement cs = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + List formats = cs + .getFormat() + .stream() + .map(t -> t.getCode()) + .collect(Collectors.toList()); + assertThat(formats.toString(), formats, containsInAnyOrder( + "application/fhir+xml", + "xml", + "application/fhir+json", + "json", + "application/x-turtle", + "ttl" + )); + } + + + @Test + public void testConditionalOperations() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ConditionalProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + ourLog.info(conf); + + assertEquals(2, conformance.getRest().get(0).getResource().size()); + CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); + assertEquals("Patient", res.getType()); + + assertTrue(res.getConditionalCreate()); + assertEquals(ConditionalDeleteStatus.MULTIPLE, res.getConditionalDelete()); + assertTrue(res.getConditionalUpdate()); + } + + private RequestDetails createRequestDetails(RestfulServer theServer) { + ServletRequestDetails retVal = new ServletRequestDetails(); + retVal.setServer(theServer); + retVal.setFhirServerBase("http://localhost/baseR4"); + return retVal; + } + + @Test + public void testExtendedOperationReturningBundle() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestResourceComponent patient = conformance.getRestFirstRep().getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + assertEquals(1, patient.getOperation().size()); + assertEquals("everything", patient.getOperation().get(0).getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Patient-i-everything", patient.getOperation().get(0).getDefinition()); + + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); + validate(opDef); + assertEquals("everything", opDef.getCode()); + assertThat(opDef.getSystem(), is(false)); + assertThat(opDef.getType(), is(false)); + assertThat(opDef.getInstance(), is(true)); + } - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); + @Test + public void testExtendedOperationReturningBundleOperation() throws Exception { - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { - }; - rs.setServerConformanceProvider(sc); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ProviderWithExtendedOperationReturningBundle()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - rs.init(createServletConfig()); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); - validate(opDef); + rs.init(MockServletUtil.createServletConfig()); - String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef); - ourLog.info(conf); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-everything"), createRequestDetails(rs)); + validate(opDef); - assertEquals("everything", opDef.getCode()); - assertEquals(false, opDef.getAffectsState()); - } + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef); + ourLog.info(conf); - @Test - public void testInstanceHistorySupported() throws Exception { + assertEquals("everything", opDef.getCode()); + assertEquals(false, opDef.getAffectsState()); + } - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new InstanceHistoryProvider()); + @Test + public void testInstanceHistorySupported() throws Exception { - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new InstanceHistoryProvider()); - rs.init(createServletConfig()); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + rs.init(MockServletUtil.createServletConfig()); - conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); - assertThat(conf, containsString("")); - } + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - @Test - public void testMultiOptionalDocumentation() throws Exception { + conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); + assertThat(conf, containsString("")); + } - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new MultiOptionalProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - boolean found = false; - Collection resourceBindings = rs.getResourceBindings(); - for (ResourceBinding resourceBinding : resourceBindings) { - if (resourceBinding.getResourceName().equals("Patient")) { - List> methodBindings = resourceBinding.getMethodBindings(); - SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); - SearchParameter param = (SearchParameter) binding.getParameters().iterator().next(); - assertEquals("The patient's identifier", param.getDescription()); - found = true; - } - } - - assertTrue(found); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); - - assertThat(conf, containsString("")); - assertThat(conf, containsString("")); - assertThat(conf, containsString("")); - } - - @Test - public void testNonConditionalOperations() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new NonConditionalProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); - assertEquals("Patient", res.getType()); - - assertNull(res.getConditionalCreateElement().getValue()); - assertNull(res.getConditionalDeleteElement().getValue()); - assertNull(res.getConditionalUpdateElement().getValue()); - } - - /** - * See #379 - */ - @Test - public void testOperationAcrossMultipleTypes() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new MultiTypePatientProvider(), new MultiTypeEncounterProvider()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - validate(conformance); - - assertEquals(4, conformance.getRest().get(0).getOperation().size()); - List operationNames = toOperationNames(conformance.getRest().get(0).getOperation()); - assertThat(operationNames, containsInAnyOrder("someOp", "validate", "someOp", "validate")); - - List operationIdParts = toOperationIdParts(conformance.getRest().get(0).getOperation()); - assertThat(operationIdParts, containsInAnyOrder("Patient-i-someOp", "Encounter-i-someOp", "Patient-i-validate", "Encounter-i-validate")); - - { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-someOp"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set types = toStrings(opDef.getResource()); - assertEquals("someOp", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); - assertEquals(2, opDef.getParameter().size()); - assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); - assertEquals("date", opDef.getParameter().get(0).getType()); - assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Patient", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Encounter-i-someOp"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set types = toStrings(opDef.getResource()); - assertEquals("someOp", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Encounter")); - assertEquals(2, opDef.getParameter().size()); - assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); - assertEquals("date", opDef.getParameter().get(0).getType()); - assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); - assertEquals("Encounter", opDef.getParameter().get(1).getType()); - } - { - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/Patient-i-validate"), createRequestDetails(rs)); - validate(opDef); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); - Set types = toStrings(opDef.getResource()); - assertEquals("validate", opDef.getCode()); - assertEquals(true, opDef.getInstance()); - assertEquals(false, opDef.getSystem()); - assertThat(types, containsInAnyOrder("Patient")); - assertEquals(1, opDef.getParameter().size()); - assertEquals("resource", opDef.getParameter().get(0).getName()); - assertEquals("Patient", opDef.getParameter().get(0).getType()); - } - } - - @Test - public void testOperationDocumentation() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SearchProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - String conf = validate(conformance); - - assertThat(conf, containsString("")); - assertThat(conf, containsString("")); - - } - - @Test - public void testOperationOnNoTypes() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new PlainProviderWithExtendedOperationOnNoType()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { - @Override - public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { - return (CapabilityStatement) super.getServerConformance(theRequest, createRequestDetails(rs)); - } - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/-is-plain"), createRequestDetails(rs)); - validate(opDef); - - assertEquals("plain", opDef.getCode()); - assertEquals(false, opDef.getAffectsState()); - assertEquals(3, opDef.getParameter().size()); - - assertTrue(opDef.getParameter().get(0).hasName()); - assertEquals("start", opDef.getParameter().get(0).getName()); - assertEquals("in", opDef.getParameter().get(0).getUse().toCode()); - assertEquals("0", opDef.getParameter().get(0).getMinElement().getValueAsString()); - assertEquals("date", opDef.getParameter().get(0).getTypeElement().getValueAsString()); - - assertEquals("out1", opDef.getParameter().get(2).getName()); - assertEquals("out", opDef.getParameter().get(2).getUse().toCode()); - assertEquals("1", opDef.getParameter().get(2).getMinElement().getValueAsString()); - assertEquals("2", opDef.getParameter().get(2).getMaxElement().getValueAsString()); - assertEquals("string", opDef.getParameter().get(2).getTypeElement().getValueAsString()); - - assertThat(opDef.getSystem(), is(true)); - assertThat(opDef.getType(), is(false)); - assertThat(opDef.getInstance(), is(true)); - } - - @Test - public void testProviderWithRequiredAndOptional() throws Exception { - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ProviderWithRequiredAndOptional()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - CapabilityStatementRestComponent rest = conformance.getRest().get(0); - CapabilityStatementRestResourceComponent res = rest.getResource().get(0); - assertEquals("DiagnosticReport", res.getType()); - - assertEquals("subject.identifier", res.getSearchParam().get(0).getName()); + @Test + public void testMultiOptionalDocumentation() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MultiOptionalProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + boolean found = false; + Collection resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + SearchParameter param = (SearchParameter) binding.getParameters().iterator().next(); + assertEquals("The patient's identifier", param.getDescription()); + found = true; + } + } + + assertTrue(found); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); + + assertThat(conf, containsString("")); + assertThat(conf, containsString("")); + assertThat(conf, containsString("")); + } + + @Test + public void testNonConditionalOperations() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new NonConditionalProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestResourceComponent res = conformance.getRest().get(0).getResource().get(1); + assertEquals("Patient", res.getType()); + + assertNull(res.getConditionalCreateElement().getValue()); + assertNull(res.getConditionalDeleteElement().getValue()); + assertNull(res.getConditionalUpdateElement().getValue()); + } + + + @Test + public void testOperationParameterDocumentation() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MultiTypePatientProvider(), new MultiTypeEncounterProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + IdType operationId = new IdType("OperationDefinition/EncounterPatient-i-someOp"); + RequestDetails requestDetails = createRequestDetails(rs); + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(operationId, requestDetails); + validate(opDef); + Set types = toStrings(opDef.getResource()); + assertEquals("someOp", opDef.getCode()); + assertEquals(true, opDef.getInstance()); + assertEquals(false, opDef.getSystem()); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); + assertEquals(2, opDef.getParameter().size()); + assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); + assertEquals("date", opDef.getParameter().get(0).getType()); + assertEquals("Start description", opDef.getParameter().get(0).getDocumentation()); + + List exampleExtensions = opDef.getParameter().get(0).getExtensionsByUrl(HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE); + assertEquals(2, exampleExtensions.size()); + assertEquals("2001", exampleExtensions.get(0).getValueAsPrimitive().getValueAsString()); + assertEquals("2002", exampleExtensions.get(1).getValueAsPrimitive().getValueAsString()); + } + + + /** + * See #379 + */ + @Test + public void testOperationAcrossMultipleTypes() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MultiTypePatientProvider(), new MultiTypeEncounterProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(conformance); + + CapabilityStatementRestResourceComponent patient = conformance.getRestFirstRep().getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + assertEquals(2, patient.getOperation().size()); + assertThat(toOperationNames(patient.getOperation()), containsInAnyOrder("someOp", "validate")); + assertThat(toOperationDefinitions(patient.getOperation()), containsInAnyOrder("http://localhost/baseR4/OperationDefinition/EncounterPatient-i-someOp", "http://localhost/baseR4/OperationDefinition/EncounterPatient-i-validate")); + + CapabilityStatementRestResourceComponent encounter = conformance.getRestFirstRep().getResource().stream().filter(t -> t.getType().equals("Encounter")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + assertEquals(2, encounter.getOperation().size()); + assertThat(toOperationNames(encounter.getOperation()), containsInAnyOrder("someOp", "validate")); + assertThat(toOperationDefinitions(encounter.getOperation()).toString(), toOperationDefinitions(encounter.getOperation()), containsInAnyOrder("http://localhost/baseR4/OperationDefinition/EncounterPatient-i-someOp", "http://localhost/baseR4/OperationDefinition/EncounterPatient-i-validate")); + + + { + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/EncounterPatient-i-someOp"), createRequestDetails(rs)); + validate(opDef); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); + Set types = toStrings(opDef.getResource()); + assertEquals("someOp", opDef.getCode()); + assertEquals(true, opDef.getInstance()); + assertEquals(false, opDef.getSystem()); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); + assertEquals(2, opDef.getParameter().size()); + assertEquals("someOpParam1", opDef.getParameter().get(0).getName()); + assertEquals("date", opDef.getParameter().get(0).getType()); + assertEquals("someOpParam2", opDef.getParameter().get(1).getName()); + assertEquals("Resource", opDef.getParameter().get(1).getType()); + } + { + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType("OperationDefinition/EncounterPatient-i-validate"), createRequestDetails(rs)); + validate(opDef); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(opDef)); + Set types = toStrings(opDef.getResource()); + assertEquals("validate", opDef.getCode()); + assertEquals(true, opDef.getInstance()); + assertEquals(false, opDef.getSystem()); + assertThat(types, containsInAnyOrder("Patient", "Encounter")); + assertEquals(1, opDef.getParameter().size()); + assertEquals("resource", opDef.getParameter().get(0).getName()); + assertEquals("Resource", opDef.getParameter().get(0).getType()); + } + } + + @Test + public void testOperationDocumentation() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SearchProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + String conf = validate(conformance); + + assertThat(conf, containsString("")); + assertThat(conf, containsString("")); + + } + + @Test + public void testProviderWithRequiredAndOptional() throws Exception { + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ProviderWithRequiredAndOptional()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestComponent rest = conformance.getRest().get(0); + CapabilityStatementRestResourceComponent res = rest.getResource().get(0); + assertEquals("DiagnosticReport", res.getType()); + + assertEquals("subject.identifier", res.getSearchParam().get(0).getName()); // assertEquals("identifier", res.getSearchParam().get(0).getChain().get(0).getValue()); - assertEquals(DiagnosticReport.SP_CODE, res.getSearchParam().get(1).getName()); + assertEquals(DiagnosticReport.SP_CODE, res.getSearchParam().get(1).getName()); - assertEquals(DiagnosticReport.SP_DATE, res.getSearchParam().get(2).getName()); + assertEquals(DiagnosticReport.SP_DATE, res.getSearchParam().get(2).getName()); - assertEquals(1, res.getSearchInclude().size()); - assertEquals("DiagnosticReport.result", res.getSearchInclude().get(0).getValue()); - } + assertEquals(1, res.getSearchInclude().size()); + assertEquals("DiagnosticReport.result", res.getSearchInclude().get(0).getValue()); + } - @Test - public void testReadAndVReadSupported() throws Exception { + @Test + public void testReadAndVReadSupported() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new VreadProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new VreadProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - assertThat(conf, containsString("")); - assertThat(conf, containsString("")); - } + assertThat(conf, containsString("")); + assertThat(conf, containsString("")); + } - @Test - public void testReadSupported() throws Exception { + @Test + public void testReadSupported() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new ReadProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new ReadProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); - ourLog.info(conf); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance); + ourLog.info(conf); - conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); - assertThat(conf, not(containsString(""))); - assertThat(conf, containsString("")); - } + conf = myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(conformance); + assertThat(conf, not(containsString(""))); + assertThat(conf, containsString("")); + } - @Test - public void testSearchParameterDocumentation() throws Exception { + @Test + public void testSearchParameterDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SearchProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SearchProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - boolean found = false; - Collection resourceBindings = rs.getResourceBindings(); - for (ResourceBinding resourceBinding : resourceBindings) { - if (resourceBinding.getResourceName().equals("Patient")) { - List> methodBindings = resourceBinding.getMethodBindings(); - SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); - for (IParameter next : binding.getParameters()) { - SearchParameter param = (SearchParameter) next; - if (param.getDescription().contains("The patient's identifier (MRN or other card number")) { - found = true; - } - } - found = true; - } - } - assertTrue(found); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + boolean found = false; + Collection resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + for (IParameter next : binding.getParameters()) { + SearchParameter param = (SearchParameter) next; + if (param.getDescription().contains("The patient's identifier (MRN or other card number")) { + found = true; + } + } + found = true; + } + } + assertTrue(found); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + String conf = validate(conformance); - assertThat(conf, containsString("")); - assertThat(conf, containsString("")); + assertThat(conf, containsString("")); + assertThat(conf, containsString("")); - } + } - @Test - public void testFormatIncludesSpecialNonMediaTypeFormats() throws ServletException { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SearchProvider()); + @Test + public void testFormatIncludesSpecialNonMediaTypeFormats() throws ServletException { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SearchProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); - CapabilityStatement serverConformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + rs.init(MockServletUtil.createServletConfig()); + CapabilityStatement serverConformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - List formatCodes = serverConformance.getFormat().stream().map(c -> c.getCode()).collect(Collectors.toList()); + List formatCodes = serverConformance.getFormat().stream().map(c -> c.getCode()).collect(Collectors.toList()); - assertThat(formatCodes, hasItem(Constants.FORMAT_XML)); - assertThat(formatCodes, hasItem(Constants.FORMAT_JSON)); - assertThat(formatCodes, hasItem(Constants.CT_FHIR_JSON_NEW)); - assertThat(formatCodes, hasItem(Constants.CT_FHIR_XML_NEW)); - } + assertThat(formatCodes, hasItem(Constants.FORMAT_XML)); + assertThat(formatCodes, hasItem(Constants.FORMAT_JSON)); + assertThat(formatCodes, hasItem(Constants.CT_FHIR_JSON_NEW)); + assertThat(formatCodes, hasItem(Constants.CT_FHIR_XML_NEW)); + } - /** - * See #286 - */ - @Test - public void testSearchReferenceParameterDocumentation() throws Exception { + /** + * See #286 + */ + @Test + public void testSearchReferenceParameterDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new PatientResourceProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new PatientResourceProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - boolean found = false; - Collection resourceBindings = rs.getResourceBindings(); - for (ResourceBinding resourceBinding : resourceBindings) { - if (resourceBinding.getResourceName().equals("Patient")) { - List> methodBindings = resourceBinding.getMethodBindings(); - SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); - SearchParameter param = (SearchParameter) binding.getParameters().get(25); - assertEquals("The organization at which this person is a patient", param.getDescription()); - found = true; - } - } - assertTrue(found); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + boolean found = false; + Collection resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + SearchParameter param = (SearchParameter) binding.getParameters().get(25); + assertEquals("The organization at which this person is a patient", param.getDescription()); + found = true; + } + } + assertTrue(found); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + String conf = validate(conformance); - } + } - /** - * See #286 - */ - @Test - public void testSearchReferenceParameterWithWhitelistDocumentation() throws Exception { + /** + * See #286 + */ + @Test + public void testSearchReferenceParameterWithWhitelistDocumentation() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SearchProviderWithWhitelist()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SearchProviderWithWhitelist()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - boolean found = false; - Collection resourceBindings = rs.getResourceBindings(); - for (ResourceBinding resourceBinding : resourceBindings) { - if (resourceBinding.getResourceName().equals("Patient")) { - List> methodBindings = resourceBinding.getMethodBindings(); - SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); - SearchParameter param = (SearchParameter) binding.getParameters().get(0); - assertEquals("The organization at which this person is a patient", param.getDescription()); - found = true; - } - } - assertTrue(found); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + boolean found = false; + Collection resourceBindings = rs.getResourceBindings(); + for (ResourceBinding resourceBinding : resourceBindings) { + if (resourceBinding.getResourceName().equals("Patient")) { + List> methodBindings = resourceBinding.getMethodBindings(); + SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0); + SearchParameter param = (SearchParameter) binding.getParameters().get(0); + assertEquals("The organization at which this person is a patient", param.getDescription()); + found = true; + } + } + assertTrue(found); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + String conf = validate(conformance); - CapabilityStatementRestResourceComponent resource = findRestResource(conformance, "Patient"); + CapabilityStatementRestResourceComponent resource = findRestResource(conformance, "Patient"); - CapabilityStatementRestResourceSearchParamComponent param = resource.getSearchParam().get(0); + CapabilityStatementRestResourceSearchParamComponent param = resource.getSearchParam().get(0); // assertEquals("bar", param.getChain().get(0).getValue()); // assertEquals("foo", param.getChain().get(1).getValue()); // assertEquals(2, param.getChain().size()); - } + } - @Test - public void testSearchReferenceParameterWithList() throws Exception { + @Test + public void testSearchReferenceParameterWithList() throws Exception { - RestfulServer rsNoType = new RestfulServer(myCtx) { - @Override - public RestfulServerConfiguration createConfiguration() { - RestfulServerConfiguration retVal = super.createConfiguration(); - retVal.setConformanceDate(new InstantDt("2011-02-22T11:22:33Z")); - return retVal; - } - }; - rsNoType.registerProvider(new SearchProviderWithListNoType()); - ServerCapabilityStatementProvider scNoType = new ServerCapabilityStatementProvider(rsNoType); - rsNoType.setServerConformanceProvider(scNoType); - rsNoType.init(createServletConfig()); + RestfulServer rsNoType = new RestfulServer(myCtx) { + @Override + public RestfulServerConfiguration createConfiguration() { + RestfulServerConfiguration retVal = super.createConfiguration(); + retVal.setConformanceDate(new InstantDt("2011-02-22T11:22:33Z")); + return retVal; + } + }; + rsNoType.registerProvider(new SearchProviderWithListNoType()); + ServerCapabilityStatementProvider scNoType = new ServerCapabilityStatementProvider(rsNoType); + rsNoType.setServerConformanceProvider(scNoType); + rsNoType.init(MockServletUtil.createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType)); - conformance.setId(""); - String confNoType = validate(conformance); + CapabilityStatement conformance = (CapabilityStatement) scNoType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsNoType)); + conformance.setId(""); + String confNoType = validate(conformance); - RestfulServer rsWithType = new RestfulServer(myCtx) { - @Override - public RestfulServerConfiguration createConfiguration() { - RestfulServerConfiguration retVal = super.createConfiguration(); - retVal.setConformanceDate(new InstantDt("2011-02-22T11:22:33Z")); - return retVal; - } - }; - rsWithType.registerProvider(new SearchProviderWithListWithType()); - ServerCapabilityStatementProvider scWithType = new ServerCapabilityStatementProvider(rsWithType); - rsWithType.setServerConformanceProvider(scWithType); - rsWithType.init(createServletConfig()); + RestfulServer rsWithType = new RestfulServer(myCtx) { + @Override + public RestfulServerConfiguration createConfiguration() { + RestfulServerConfiguration retVal = super.createConfiguration(); + retVal.setConformanceDate(new InstantDt("2011-02-22T11:22:33Z")); + return retVal; + } + }; + rsWithType.registerProvider(new SearchProviderWithListWithType()); + ServerCapabilityStatementProvider scWithType = new ServerCapabilityStatementProvider(rsWithType); + rsWithType.setServerConformanceProvider(scWithType); + rsWithType.init(MockServletUtil.createServletConfig()); - CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType)); - conformanceWithType.setId(""); - String confWithType = validate(conformanceWithType); + CapabilityStatement conformanceWithType = (CapabilityStatement) scWithType.getServerConformance(createHttpServletRequest(), createRequestDetails(rsWithType)); + conformanceWithType.setId(""); + String confWithType = validate(conformanceWithType); - assertEquals(confNoType, confWithType); - assertThat(confNoType, containsString("")); - } + assertEquals(confNoType, confWithType); + assertThat(confNoType, containsString("")); + } - @Test - public void testSystemHistorySupported() throws Exception { + @Test + public void testSystemHistorySupported() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new SystemHistoryProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new SystemHistoryProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - assertThat(conf, containsString("")); - } + assertThat(conf, containsString("")); + } - @Test - public void testTypeHistorySupported() throws Exception { + @Test + public void testTypeHistorySupported() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new TypeHistoryProvider()); + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new TypeHistoryProvider()); - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); - rs.init(createServletConfig()); + rs.init(MockServletUtil.createServletConfig()); - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - String conf = validate(conformance); + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + String conf = validate(conformance); - assertThat(conf, containsString("")); - } + assertThat(conf, containsString("")); + } + + @Test + public void testStaticIncludeChains() throws Exception { + + class MyProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return DiagnosticReport.class; + } + + @Search + public List search(@RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_FAMILY) StringParam lastName, + @RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_GIVEN) StringParam firstName, + @RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_BIRTHDATE) DateParam dob, + @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam range) { + return null; + } + + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MyProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(opDef); + + CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().get(0); + assertEquals("DiagnosticReport", resource.getType()); + List searchParamNames = resource.getSearchParam().stream().map(t -> t.getName()).collect(Collectors.toList()); + assertThat(searchParamNames, containsInAnyOrder("patient.birthdate", "patient.family", "patient.given", "date")); + } + + @Test + public void testGraphQLOperation() throws Exception { + + class MyProvider { + + @Description(value = "This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.") + @GraphQL(type = RequestTypeEnum.GET) + public String processGraphQlGetRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String queryUrl) { + throw new IllegalStateException(); + } + + @Description(value = "This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.") + @GraphQL(type = RequestTypeEnum.POST) + public String processGraphQlPostRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryBody String queryBody) { + throw new IllegalStateException(); + } - @Test - public void testStaticIncludeChains() throws Exception { + } - class MyProvider implements IResourceProvider { + RestfulServer rs = new RestfulServer(myCtx); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + rs.registerProvider(new MyProvider()); + rs.registerProvider(new HashMapResourceProvider<>(myCtx, Patient.class)); + rs.registerProvider(new HashMapResourceProvider<>(myCtx, Observation.class)); - @Override - public Class getResourceType() { - return DiagnosticReport.class; - } + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); - @Search - public List search(@RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_FAMILY) StringParam lastName, - @RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_GIVEN) StringParam firstName, - @RequiredParam(name = DiagnosticReport.SP_PATIENT + "." + Patient.SP_BIRTHDATE) DateParam dob, - @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam range) { - return null; - } + rs.init(MockServletUtil.createServletConfig()); - } - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new MyProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - validate(opDef); - - CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().get(0); - assertEquals("DiagnosticReport", resource.getType()); - List searchParamNames = resource.getSearchParam().stream().map(t -> t.getName()).collect(Collectors.toList()); - assertThat(searchParamNames, containsInAnyOrder("patient.birthdate", "patient.family", "patient.given", "date")); - } + CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - @Test - public void testIncludeLastUpdatedSearchParam() throws Exception { + validate(opDef); - class MyProvider implements IResourceProvider { + // On Patient Resource + CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + CapabilityStatementRestResourceOperationComponent graphQlOperation = resource.getOperation().stream().filter(t -> t.getName().equals("graphql")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + assertEquals("graphql", graphQlOperation.getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-graphql", graphQlOperation.getDefinition()); + assertEquals("This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.", graphQlOperation.getDocumentation()); + + // On Patient Resource + resource = opDef.getRest().get(0).getResource().stream().filter(t -> t.getType().equals("Observation")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + graphQlOperation = resource.getOperation().stream().filter(t -> t.getName().equals("graphql")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + assertEquals("graphql", graphQlOperation.getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-graphql", graphQlOperation.getDefinition()); + assertEquals("This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.", graphQlOperation.getDocumentation()); + + // At Server Level + CapabilityStatementRestComponent rest = opDef.getRest().get(0); + graphQlOperation = rest.getOperation().stream().filter(t -> t.getName().equals("graphql")).findAny().orElseThrow(() -> new IllegalArgumentException()); + assertEquals("graphql", graphQlOperation.getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-graphql", graphQlOperation.getDefinition()); + assertEquals("This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.", graphQlOperation.getDocumentation()); + + // Fetch OperationDefinition + IdType id = new IdType("http://localhost/baseR4/OperationDefinition/Global-is-graphql"); + RequestDetails requestDetails = createRequestDetails(rs); + OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(id, requestDetails); + assertEquals("Graphql", operationDefinition.getName()); + assertEquals("graphql", operationDefinition.getCode()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-graphql", operationDefinition.getUrl()); + assertEquals("This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.", operationDefinition.getDescription()); + assertTrue(operationDefinition.getSystem()); + assertFalse(operationDefinition.getType()); + assertTrue(operationDefinition.getInstance()); + } + + + @Test + public void testPlainProviderGlobalSystemAndInstanceOperations() throws Exception { + + class MyProvider { + + @Description( + value = "This operation examines two resource versions (can be two versions of the same resource, or two different resources) and generates a FHIR Patch document showing the differences.", + shortDefinition = "Comparte two resources or two versions of a single resource") + @Operation(name = ProviderConstants.DIFF_OPERATION_NAME, global = true, idempotent = true) + public IBaseParameters diff( + @IdParam IIdType theResourceId, + + @Description(value = "The resource ID and version to diff from", example = "Patient/example/version/1") + @OperationParam(name = ProviderConstants.DIFF_FROM_VERSION_PARAMETER, typeName = "string", min = 0, max = 1) + IPrimitiveType theFromVersion, + + @Description(value = "Should differences in the Resource.meta element be included in the diff", example = "false") + @OperationParam(name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER, typeName = "boolean", min = 0, max = 1) + IPrimitiveType theIncludeMeta, + RequestDetails theRequestDetails) { + throw new IllegalStateException(); + } + + @Description("This operation examines two resource versions (can be two versions of the same resource, or two different resources) and generates a FHIR Patch document showing the differences.") + @Operation(name = ProviderConstants.DIFF_OPERATION_NAME, idempotent = true) + public IBaseParameters diff( + @Description(value = "The resource ID and version to diff from", example = "Patient/example/version/1") + @OperationParam(name = ProviderConstants.DIFF_FROM_PARAMETER, typeName = "id", min = 1, max = 1) + IIdType theFromVersion, + + @Description(value = "The resource ID and version to diff to", example = "Patient/example/version/2") + @OperationParam(name = ProviderConstants.DIFF_TO_PARAMETER, typeName = "id", min = 1, max = 1) + IIdType theToVersion, + + @Description(value = "Should differences in the Resource.meta element be included in the diff", example = "false") + @OperationParam(name = ProviderConstants.DIFF_INCLUDE_META_PARAMETER, typeName = "boolean", min = 0, max = 1) + IPrimitiveType theIncludeMeta, + RequestDetails theRequestDetails) { + throw new IllegalStateException(); + } + + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + rs.registerProvider(new MyProvider()); + rs.registerProvider(new HashMapResourceProvider<>(myCtx, Patient.class)); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(opDef); + + // On Patient Resource + CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + CapabilityStatementRestResourceOperationComponent operation = resource.getOperation().stream().filter(t -> t.getName().equals("diff")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + assertEquals("diff", operation.getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-diff", operation.getDefinition()); + assertEquals("This operation examines two resource versions (can be two versions of the same resource, or two different resources) and generates a FHIR Patch document showing the differences.", operation.getDocumentation()); + + // At Server Level + CapabilityStatementRestComponent rest = opDef.getRest().get(0); + operation = rest.getOperation().stream().filter(t -> t.getName().equals("diff")).findAny().orElse(null); + assertEquals("diff", operation.getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-diff", operation.getDefinition()); + assertEquals("This operation examines two resource versions (can be two versions of the same resource, or two different resources) and generates a FHIR Patch document showing the differences.", operation.getDocumentation()); + + // Fetch OperationDefinition + IdType id = new IdType("http://localhost/baseR4/OperationDefinition/Global-is-diff"); + RequestDetails requestDetails = createRequestDetails(rs); + OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(id, requestDetails); + assertEquals("Diff", operationDefinition.getName()); + assertEquals("diff", operationDefinition.getCode()); + assertEquals("http://localhost/baseR4/OperationDefinition/Global-is-diff", operationDefinition.getUrl()); + assertEquals("This operation examines two resource versions (can be two versions of the same resource, or two different resources) and generates a FHIR Patch document showing the differences.", operationDefinition.getDescription()); + assertTrue(operationDefinition.getSystem()); + assertFalse(operationDefinition.getType()); + assertTrue(operationDefinition.getInstance()); + } + + @Test + public void testResourceProviderTypeAndInstanceLevelOperations() throws Exception { + + class MyProvider implements IResourceProvider { + + @Description("This is the expunge operation") + @Operation(name = "expunge", idempotent = false, returnParameters = { + @OperationParam(name = "count", typeName = "integer") + }) + public IBaseParameters expunge( + @IdParam IIdType theIdParam, + @OperationParam(name = "limit", typeName = "integer") IPrimitiveType theLimit, + @OperationParam(name = "deleted", typeName = "boolean") IPrimitiveType theExpungeDeletedResources, + @OperationParam(name = "previous", typeName = "boolean") IPrimitiveType theExpungeOldVersions, + RequestDetails theRequest) { + throw new UnsupportedOperationException(); + } + + @Description("This is the expunge operation") + @Operation(name = "expunge", idempotent = false, returnParameters = { + @OperationParam(name = "count", typeName = "integer") + }) + public IBaseParameters expunge( + @OperationParam(name = "limit", typeName = "integer") IPrimitiveType theLimit, + @OperationParam(name = "deleted", typeName = "boolean") IPrimitiveType theExpungeDeletedResources, + @OperationParam(name = "previous", typeName = "boolean") IPrimitiveType theExpungeOldVersions, + RequestDetails theRequest) { + throw new UnsupportedOperationException(); + } + + @Override + public Class getResourceType() { + return Patient.class; + } + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + rs.registerProvider(new MyProvider()); + rs.registerProvider(new HashMapResourceProvider<>(myCtx, Patient.class)); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(opDef); + + // On Patient Resource + CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + List expungeResourceOperations = resource.getOperation().stream().filter(t -> t.getName().equals("expunge")).collect(Collectors.toList()); + assertEquals(1, expungeResourceOperations.size()); + CapabilityStatementRestResourceOperationComponent operation = expungeResourceOperations.get(0); + assertEquals("expunge", operation.getName()); + assertEquals("http://localhost/baseR4/OperationDefinition/Patient-it-expunge", operation.getDefinition()); + assertEquals("This is the expunge operation", operation.getDocumentation()); + + // At Server Level + CapabilityStatementRestComponent rest = opDef.getRest().get(0); + operation = rest.getOperation().stream().filter(t -> t.getName().equals("expunge")).findAny().orElse(null); + assertNull(operation); + + // Fetch OperationDefinition + IdType id = new IdType("http://localhost/baseR4/OperationDefinition/Patient-it-expunge"); + RequestDetails requestDetails = createRequestDetails(rs); + OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(id, requestDetails); + assertEquals("Expunge", operationDefinition.getName()); + assertEquals("expunge", operationDefinition.getCode()); + assertEquals("http://localhost/baseR4/OperationDefinition/Patient-it-expunge", operationDefinition.getUrl()); + assertEquals("This is the expunge operation", operationDefinition.getDescription()); + assertFalse(operationDefinition.getSystem()); + assertTrue(operationDefinition.getType()); + assertTrue(operationDefinition.getInstance()); + + } + + @Test + public void testIncludeLastUpdatedSearchParam() throws Exception { + + class MyProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return DiagnosticReport.class; + } + + @Search + public List search(@OptionalParam(name = DiagnosticReport.SP_DATE) + DateRangeParam range, + + @Description(shortDefinition = "Only return resources which were last updated as specified by the given range") + @OptionalParam(name = "_lastUpdated") + DateRangeParam theLastUpdated + ) { + return null; + } + + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new MyProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { + }; + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(opDef); + + CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().get(0); + assertEquals("DiagnosticReport", resource.getType()); + List searchParamNames = resource.getSearchParam().stream().map(t -> t.getName()).collect(Collectors.toList()); + assertThat(searchParamNames, containsInAnyOrder("date", "_lastUpdated")); + } + + @Test + public void testSystemLevelNamedQueryWithParameters() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new NamedQueryPlainProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestResourceComponent patient = conformance.getRestFirstRep().getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + CapabilityStatementRestResourceOperationComponent operationComponent = patient.getOperation().get(0); + assertThat(operationComponent.getName(), is(NamedQueryPlainProvider.QUERY_NAME)); + + String operationReference = operationComponent.getDefinition(); + assertThat(operationReference, not(nullValue())); + + OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); + validate(operationDefinition); + assertThat(operationDefinition.getCode(), is(NamedQueryPlainProvider.QUERY_NAME)); + assertThat(operationDefinition.getName(), is("TestQuery")); + assertThat(operationDefinition.getStatus(), is(PublicationStatus.ACTIVE)); + assertThat(operationDefinition.getKind(), is(OperationKind.QUERY)); + assertThat(operationDefinition.getDescription(), is(NamedQueryPlainProvider.DESCRIPTION)); + assertThat(operationDefinition.getAffectsState(), is(false)); + assertThat("A system level search has no target resources", operationDefinition.getResource(), is(empty())); + assertThat(operationDefinition.getSystem(), is(true)); + assertThat(operationDefinition.getType(), is(false)); + assertThat(operationDefinition.getInstance(), is(false)); + List parameters = operationDefinition.getParameter(); + assertThat(parameters.size(), is(1)); + OperationDefinitionParameterComponent param = parameters.get(0); + assertThat(param.getName(), is(NamedQueryPlainProvider.SP_QUANTITY)); + assertThat(param.getType(), is("string")); + assertThat(param.getSearchTypeElement().asStringValue(), is(RestSearchParameterTypeEnum.QUANTITY.getCode())); + assertThat(param.getMin(), is(1)); + assertThat(param.getMax(), is("1")); + assertThat(param.getUse(), is(OperationParameterUse.IN)); + } + + @Test + public void testResourceLevelNamedQueryWithParameters() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new NamedQueryResourceProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + validate(conformance); + + CapabilityStatementRestResourceComponent restComponent = conformance.getRestFirstRep().getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + CapabilityStatementRestResourceOperationComponent operationComponent = restComponent.getOperation().get(0); + String operationReference = operationComponent.getDefinition(); + assertThat(operationReference, not(nullValue())); + + OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); + validate(operationDefinition); + assertThat("The operation name should be the code if no description is set", operationDefinition.getName(), is("TestQuery")); + String patientResourceName = "Patient"; + assertThat("A resource level search targets the resource of the provider it's defined in", operationDefinition.getResource().get(0).getValue(), is(patientResourceName)); + assertThat(operationDefinition.getSystem(), is(false)); + assertThat(operationDefinition.getType(), is(true)); + assertThat(operationDefinition.getInstance(), is(false)); + List parameters = operationDefinition.getParameter(); + assertThat(parameters.size(), is(1)); + OperationDefinitionParameterComponent param = parameters.get(0); + assertThat(param.getName(), is(NamedQueryResourceProvider.SP_PARAM)); + assertThat(param.getType(), is("string")); + assertThat(param.getSearchTypeElement().asStringValue(), is(RestSearchParameterTypeEnum.STRING.getCode())); + assertThat(param.getMin(), is(0)); + assertThat(param.getMax(), is("1")); + assertThat(param.getUse(), is(OperationParameterUse.IN)); + + List patientResource = restComponent.getSearchParam(); + assertThat("Named query parameters should not appear in the resource search params", patientResource, is(empty())); + } + + @Test + public void testExtendedOperationAtTypeLevel() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setProviders(new TypeLevelOperationProvider()); + rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + + validate(conformance); + + CapabilityStatementRestResourceComponent patient = conformance.getRestFirstRep().getResource().stream().filter(t -> t.getType().equals("Patient")).findFirst().orElseThrow(() -> new IllegalArgumentException()); + List operations = patient.getOperation(); + assertThat(operations.size(), is(1)); + assertThat(operations.get(0).getName(), is(TypeLevelOperationProvider.OPERATION_NAME)); + + OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType(operations.get(0).getDefinition()), createRequestDetails(rs)); + validate(opDef); + assertEquals(TypeLevelOperationProvider.OPERATION_NAME, opDef.getCode()); + assertThat(opDef.getSystem(), is(false)); + assertThat(opDef.getType(), is(true)); + assertThat(opDef.getInstance(), is(false)); + } + + @Test + public void testProfiledResourceStructureDefinitionLinks() throws Exception { + RestfulServer rs = new RestfulServer(myCtx); + rs.setResourceProviders(new ProfiledPatientProvider(), new MultipleProfilesPatientProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + + List resources = conformance.getRestFirstRep().getResource(); + CapabilityStatementRestResourceComponent patientResource = resources.stream() + .filter(resource -> "Patient".equals(resource.getType())) + .findFirst().get(); + assertThat(patientResource.getProfile(), containsString(PATIENT_SUB)); + } + + @Test + public void testRevIncludes_Explicit() throws Exception { + + class PatientResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Search + public List search(@IncludeParam(reverse = true, allow = {"Observation:foo", "Provenance:bar"}) Set theRevIncludes) { + return Collections.emptyList(); + } + + } + + class ObservationResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Observation.class; + } + + @Search + public List search(@OptionalParam(name = "subject") ReferenceParam theSubject) { + return Collections.emptyList(); + } + + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + sc.setRestResourceRevIncludesEnabled(true); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + + List resources = conformance.getRestFirstRep().getResource(); + CapabilityStatementRestResourceComponent patientResource = resources.stream() + .filter(resource -> "Patient".equals(resource.getType())) + .findFirst().get(); + assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:foo", "Provenance:bar")); + } + + @Test + public void testRevIncludes_Inferred() throws Exception { + + class PatientResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Search + public List search(@IncludeParam(reverse = true) Set theRevIncludes) { + return Collections.emptyList(); + } + + } + + class ObservationResourceProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Observation.class; + } + + @Search + public List search(@OptionalParam(name = "subject") ReferenceParam theSubject) { + return Collections.emptyList(); + } + + } + + RestfulServer rs = new RestfulServer(myCtx); + rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider()); + + ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); + sc.setRestResourceRevIncludesEnabled(true); + rs.setServerConformanceProvider(sc); + + rs.init(MockServletUtil.createServletConfig()); + + CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); + ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); + + List resources = conformance.getRestFirstRep().getResource(); + CapabilityStatementRestResourceComponent patientResource = resources.stream() + .filter(resource -> "Patient".equals(resource.getType())) + .findFirst().get(); + assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:subject")); + } + + private List toOperationIdParts(List theOperation) { + ArrayList retVal = Lists.newArrayList(); + for (CapabilityStatementRestResourceOperationComponent next : theOperation) { + retVal.add(new IdType(next.getDefinition()).getIdPart()); + } + return retVal; + } + + private List toOperationNames(List theOperation) { + ArrayList retVal = Lists.newArrayList(); + for (CapabilityStatementRestResourceOperationComponent next : theOperation) { + retVal.add(next.getName()); + } + return retVal; + } + + private List toOperationDefinitions(List theOperation) { + ArrayList retVal = Lists.newArrayList(); + for (CapabilityStatementRestResourceOperationComponent next : theOperation) { + retVal.add(next.getDefinition()); + } + return retVal; + } + + private String validate(IBaseResource theResource) { + String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theResource); + ourLog.info("Def:\n{}", conf); + + ValidationResult result = myValidator.validateWithResult(conf); + OperationOutcome operationOutcome = (OperationOutcome) result.toOperationOutcome(); + String outcome = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationOutcome); + ourLog.info("Outcome: {}", outcome); + + assertTrue(result.isSuccessful(), outcome); + List warningsAndErrors = operationOutcome + .getIssue() + .stream() + .filter(t -> t.getSeverity().ordinal() <= OperationOutcome.IssueSeverity.WARNING.ordinal()) // <= because this enum has a strange order + .collect(Collectors.toList()); + assertThat(outcome, warningsAndErrors, is(empty())); + + return myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(theResource); + } + + @SuppressWarnings("unused") + public static class ConditionalProvider implements IResourceProvider { + + @Create + public MethodOutcome create(@ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) { + return null; + } + + @Delete + public MethodOutcome delete(@IdParam IdType theId, @ConditionalUrlParam(supportsMultiple = true) String theConditionalUrl) { + return null; + } + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Update + public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) { + return null; + } + + } + + @SuppressWarnings("unused") + public static class InstanceHistoryProvider implements IResourceProvider { + @Override + public Class getResourceType() { + return Patient.class; + } + + @History + public List history(@IdParam IdType theId) { + return null; + } + + } + + @SuppressWarnings("unused") + public static class MultiOptionalProvider { + + @Search(type = Patient.class) + public Patient findPatient(@Description(shortDefinition = "The patient's identifier") @OptionalParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier, + @Description(shortDefinition = "The patient's name") @OptionalParam(name = Patient.SP_NAME) StringParam theName) { + return null; + } + + } + + @SuppressWarnings("unused") + public static class MultiTypeEncounterProvider implements IResourceProvider { + + @Operation(name = "someOp") + public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, + @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) { + return null; + } + + @Override + public Class getResourceType() { + return Encounter.class; + } + + @Validate + public IBundleProvider validate(HttpServletRequest theServletRequest, @IdParam IdType theId, @ResourceParam Encounter thePatient) { + return null; + } + + } + + @SuppressWarnings("unused") + public static class MultiTypePatientProvider implements IResourceProvider { + + @Operation(name = "someOp") + public IBundleProvider everything( + HttpServletRequest theServletRequest, + + @IdParam IdType theId, + + @Description(value = "Start description", example = {"2001", "2002"}) + @OperationParam(name = "someOpParam1") DateType theStart, + + @OperationParam(name = "someOpParam2") Patient theEnd) { + return null; + } - @Override - public Class getResourceType() { - return DiagnosticReport.class; - } + @Override + public Class getResourceType() { + return Patient.class; + } + + @Validate + public IBundleProvider validate(HttpServletRequest theServletRequest, @IdParam IdType theId, @ResourceParam Patient thePatient) { + return null; + } - @Search - public List search(@OptionalParam(name = DiagnosticReport.SP_DATE) - DateRangeParam range, + } - @Description(shortDefinition = "Only return resources which were last updated as specified by the given range") - @OptionalParam(name = "_lastUpdated") - DateRangeParam theLastUpdated - ) { - return null; - } - - } - - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new MyProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs) { - }; - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement opDef = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - validate(opDef); - - CapabilityStatementRestResourceComponent resource = opDef.getRest().get(0).getResource().get(0); - assertEquals("DiagnosticReport", resource.getType()); - List searchParamNames = resource.getSearchParam().stream().map(t -> t.getName()).collect(Collectors.toList()); - assertThat(searchParamNames, containsInAnyOrder("date", "_lastUpdated")); - } - - @Test - public void testSystemLevelNamedQueryWithParameters() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new NamedQueryPlainProvider()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - CapabilityStatementRestComponent restComponent = conformance.getRest().get(0); - CapabilityStatementRestResourceOperationComponent operationComponent = restComponent.getOperation().get(0); - assertThat(operationComponent.getName(), is(NamedQueryPlainProvider.QUERY_NAME)); - - String operationReference = operationComponent.getDefinition(); - assertThat(operationReference, not(nullValue())); - - OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); - validate(operationDefinition); - assertThat(operationDefinition.getCode(), is(NamedQueryPlainProvider.QUERY_NAME)); - assertThat(operationDefinition.getName(), is("Search_" + NamedQueryPlainProvider.QUERY_NAME)); - assertThat(operationDefinition.getStatus(), is(PublicationStatus.ACTIVE)); - assertThat(operationDefinition.getKind(), is(OperationKind.QUERY)); - assertThat(operationDefinition.getDescription(), is(NamedQueryPlainProvider.DESCRIPTION)); - assertThat(operationDefinition.getAffectsState(), is(false)); - assertThat("A system level search has no target resources", operationDefinition.getResource(), is(empty())); - assertThat(operationDefinition.getSystem(), is(true)); - assertThat(operationDefinition.getType(), is(false)); - assertThat(operationDefinition.getInstance(), is(false)); - List parameters = operationDefinition.getParameter(); - assertThat(parameters.size(), is(1)); - OperationDefinitionParameterComponent param = parameters.get(0); - assertThat(param.getName(), is(NamedQueryPlainProvider.SP_QUANTITY)); - assertThat(param.getType(), is("string")); - assertThat(param.getSearchTypeElement().asStringValue(), is(RestSearchParameterTypeEnum.QUANTITY.getCode())); - assertThat(param.getMin(), is(1)); - assertThat(param.getMax(), is("1")); - assertThat(param.getUse(), is(OperationParameterUse.IN)); - } - - @Test - public void testResourceLevelNamedQueryWithParameters() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new NamedQueryResourceProvider()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - validate(conformance); - - CapabilityStatementRestComponent restComponent = conformance.getRest().get(0); - CapabilityStatementRestResourceOperationComponent operationComponent = restComponent.getOperation().get(0); - String operationReference = operationComponent.getDefinition(); - assertThat(operationReference, not(nullValue())); - - OperationDefinition operationDefinition = (OperationDefinition) sc.readOperationDefinition(new IdType(operationReference), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationDefinition)); - validate(operationDefinition); - assertThat("The operation name should be the code if no description is set", operationDefinition.getName(), is("Search_" + NamedQueryResourceProvider.QUERY_NAME)); - String patientResourceName = "Patient"; - assertThat("A resource level search targets the resource of the provider it's defined in", operationDefinition.getResource().get(0).getValue(), is(patientResourceName)); - assertThat(operationDefinition.getSystem(), is(false)); - assertThat(operationDefinition.getType(), is(true)); - assertThat(operationDefinition.getInstance(), is(false)); - List parameters = operationDefinition.getParameter(); - assertThat(parameters.size(), is(1)); - OperationDefinitionParameterComponent param = parameters.get(0); - assertThat(param.getName(), is(NamedQueryResourceProvider.SP_PARAM)); - assertThat(param.getType(), is("string")); - assertThat(param.getSearchTypeElement().asStringValue(), is(RestSearchParameterTypeEnum.STRING.getCode())); - assertThat(param.getMin(), is(0)); - assertThat(param.getMax(), is("1")); - assertThat(param.getUse(), is(OperationParameterUse.IN)); - - CapabilityStatementRestResourceComponent patientResource = restComponent.getResource().stream() - .filter(r -> patientResourceName.equals(r.getType())) - .findAny().get(); - assertThat("Named query parameters should not appear in the resource search params", patientResource.getSearchParam(), is(empty())); - } - - @Test - public void testExtendedOperationAtTypeLevel() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setProviders(new TypeLevelOperationProvider()); - rs.setServerAddressStrategy(new HardcodedServerAddressStrategy("http://localhost/baseR4")); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - - validate(conformance); - - List operations = conformance.getRest().get(0).getOperation(); - assertThat(operations.size(), is(1)); - assertThat(operations.get(0).getName(), is(TypeLevelOperationProvider.OPERATION_NAME)); - - OperationDefinition opDef = (OperationDefinition) sc.readOperationDefinition(new IdType(operations.get(0).getDefinition()), createRequestDetails(rs)); - validate(opDef); - assertEquals(TypeLevelOperationProvider.OPERATION_NAME, opDef.getCode()); - assertThat(opDef.getSystem(), is(false)); - assertThat(opDef.getType(), is(true)); - assertThat(opDef.getInstance(), is(false)); - } - - @Test - public void testProfiledResourceStructureDefinitionLinks() throws Exception { - RestfulServer rs = new RestfulServer(myCtx); - rs.setResourceProviders(new ProfiledPatientProvider(), new MultipleProfilesPatientProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); - - List resources = conformance.getRestFirstRep().getResource(); - CapabilityStatementRestResourceComponent patientResource = resources.stream() - .filter(resource -> "Patient".equals(resource.getType())) - .findFirst().get(); - assertThat(patientResource.getProfile(), containsString(PATIENT_SUB)); - } - - @Test - public void testRevIncludes_Explicit() throws Exception { - - class PatientResourceProvider implements IResourceProvider { - - @Override - public Class getResourceType() { - return Patient.class; - } - - @Search - public List search(@IncludeParam(reverse = true, allow = {"Observation:foo", "Provenance:bar"}) Set theRevIncludes) { - return Collections.emptyList(); - } - - } - - class ObservationResourceProvider implements IResourceProvider { - - @Override - public Class getResourceType() { - return Observation.class; - } - - @Search - public List search(@OptionalParam(name = "subject") ReferenceParam theSubject) { - return Collections.emptyList(); - } - - } - - RestfulServer rs = new RestfulServer(myCtx); - rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider()); - - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - sc.setRestResourceRevIncludesEnabled(true); - rs.setServerConformanceProvider(sc); - - rs.init(createServletConfig()); - - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); - - List resources = conformance.getRestFirstRep().getResource(); - CapabilityStatementRestResourceComponent patientResource = resources.stream() - .filter(resource -> "Patient".equals(resource.getType())) - .findFirst().get(); - assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:foo", "Provenance:bar")); - } + @SuppressWarnings("unused") + public static class NonConditionalProvider implements IResourceProvider { - @Test - public void testRevIncludes_Inferred() throws Exception { + @Create + public MethodOutcome create(@ResourceParam Patient thePatient) { + return null; + } - class PatientResourceProvider implements IResourceProvider { + @Delete + public MethodOutcome delete(@IdParam IdType theId) { + return null; + } - @Override - public Class getResourceType() { - return Patient.class; - } + @Override + public Class getResourceType() { + return Patient.class; + } - @Search - public List search(@IncludeParam(reverse = true) Set theRevIncludes) { - return Collections.emptyList(); - } - - } - - class ObservationResourceProvider implements IResourceProvider { - - @Override - public Class getResourceType() { - return Observation.class; - } - - @Search - public List search(@OptionalParam(name = "subject") ReferenceParam theSubject) { - return Collections.emptyList(); - } + @Update + public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient) { + return null; + } - } - - RestfulServer rs = new RestfulServer(myCtx); - rs.setResourceProviders(new PatientResourceProvider(), new ObservationResourceProvider()); + } - ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs); - sc.setRestResourceRevIncludesEnabled(true); - rs.setServerConformanceProvider(sc); + @SuppressWarnings("unused") + public static class ProviderWithExtendedOperationReturningBundle implements IResourceProvider { - rs.init(createServletConfig()); + @Operation(name = "everything", idempotent = true) + public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, + @OperationParam(name = "end") DateType theEnd) { + return null; + } - CapabilityStatement conformance = (CapabilityStatement) sc.getServerConformance(createHttpServletRequest(), createRequestDetails(rs)); - ourLog.info(myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance)); - - List resources = conformance.getRestFirstRep().getResource(); - CapabilityStatementRestResourceComponent patientResource = resources.stream() - .filter(resource -> "Patient".equals(resource.getType())) - .findFirst().get(); - assertThat(toStrings(patientResource.getSearchRevInclude()), containsInAnyOrder("Observation:subject")); - } + @Override + public Class getResourceType() { + return Patient.class; + } - private List toOperationIdParts(List theOperation) { - ArrayList retVal = Lists.newArrayList(); - for (CapabilityStatementRestResourceOperationComponent next : theOperation) { - retVal.add(new IdType(next.getDefinition()).getIdPart()); - } - return retVal; - } - - private List toOperationNames(List theOperation) { - ArrayList retVal = Lists.newArrayList(); - for (CapabilityStatementRestResourceOperationComponent next : theOperation) { - retVal.add(next.getName()); - } - return retVal; - } - - private String validate(IBaseResource theResource) { - String conf = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(theResource); - ourLog.info("Def:\n{}", conf); - - ValidationResult result = myValidator.validateWithResult(conf); - OperationOutcome operationOutcome = (OperationOutcome) result.toOperationOutcome(); - String outcome = myCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(operationOutcome); - ourLog.info("Outcome: {}", outcome); - - assertTrue(result.isSuccessful(), outcome); - List warningsAndErrors = operationOutcome - .getIssue() - .stream() - .filter(t -> t.getSeverity().ordinal() <= OperationOutcome.IssueSeverity.WARNING.ordinal()) // <= because this enum has a strange order - .collect(Collectors.toList()); - assertThat(outcome, warningsAndErrors, is(empty())); - - return myCtx.newXmlParser().setPrettyPrint(false).encodeResourceToString(theResource); - } - - @SuppressWarnings("unused") - public static class ConditionalProvider implements IResourceProvider { - - @Create - public MethodOutcome create(@ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) { - return null; - } - - @Delete - public MethodOutcome delete(@IdParam IdType theId, @ConditionalUrlParam(supportsMultiple = true) String theConditionalUrl) { - return null; - } - - @Override - public Class getResourceType() { - return Patient.class; - } - - @Update - public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient, @ConditionalUrlParam String theConditionalUrl) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class InstanceHistoryProvider implements IResourceProvider { - @Override - public Class getResourceType() { - return Patient.class; - } - - @History - public List history(@IdParam IdType theId) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class MultiOptionalProvider { - - @Search(type = Patient.class) - public Patient findPatient(@Description(shortDefinition = "The patient's identifier") @OptionalParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier, - @Description(shortDefinition = "The patient's name") @OptionalParam(name = Patient.SP_NAME) StringParam theName) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class MultiTypeEncounterProvider implements IResourceProvider { - - @Operation(name = "someOp") - public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, - @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) { - return null; - } - - @Override - public Class getResourceType() { - return Encounter.class; - } - - @Validate - public IBundleProvider validate(HttpServletRequest theServletRequest, @IdParam IdType theId, @ResourceParam Encounter thePatient) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class MultiTypePatientProvider implements IResourceProvider { - - @Operation(name = "someOp") - public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, - @OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Patient theEnd) { - return null; - } - - @Override - public Class getResourceType() { - return Patient.class; - } - - @Validate - public IBundleProvider validate(HttpServletRequest theServletRequest, @IdParam IdType theId, @ResourceParam Patient thePatient) { - return null; - } - - } - - @SuppressWarnings("unused") - public static class NonConditionalProvider implements IResourceProvider { - - @Create - public MethodOutcome create(@ResourceParam Patient thePatient) { - return null; - } + } - @Delete - public MethodOutcome delete(@IdParam IdType theId) { - return null; - } - - @Override - public Class getResourceType() { - return Patient.class; - } + @SuppressWarnings("unused") + public static class ProviderWithRequiredAndOptional { - @Update - public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient) { - return null; - } + @Description(shortDefinition = "This is a search for stuff!") + @Search + public List findDiagnosticReportsByPatient(@RequiredParam(name = DiagnosticReport.SP_SUBJECT + '.' + Patient.SP_IDENTIFIER) TokenParam thePatientId, + @OptionalParam(name = DiagnosticReport.SP_CODE) TokenOrListParam theNames, @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange, + @IncludeParam(allow = {"DiagnosticReport.result"}) Set theIncludes) throws Exception { + return null; + } - } + } - @SuppressWarnings("unused") - public static class PlainProviderWithExtendedOperationOnNoType { + @SuppressWarnings("unused") + public static class ReadProvider { - @Operation(name = "plain", idempotent = true, returnParameters = {@OperationParam(min = 1, max = 2, name = "out1", type = StringType.class)}) - public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, - @OperationParam(name = "end") DateType theEnd) { - return null; - } + @Search(type = Patient.class) + public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { + return null; + } - } + @Read(version = false) + public Patient readPatient(@IdParam IdType theId) { + return null; + } - @SuppressWarnings("unused") - public static class ProviderWithExtendedOperationReturningBundle implements IResourceProvider { + } - @Operation(name = "everything", idempotent = true) - public IBundleProvider everything(HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart, - @OperationParam(name = "end") DateType theEnd) { - return null; - } + @SuppressWarnings("unused") + public static class SearchProvider { - @Override - public Class getResourceType() { - return Patient.class; - } + @Search(type = Patient.class) + public Patient findPatient1(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { + return null; + } - } + @Search(type = Patient.class) + public Patient findPatient2( + @Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = {Patient.class}) ReferenceAndListParam theLink) { + return null; + } - @SuppressWarnings("unused") - public static class ProviderWithRequiredAndOptional { + } - @Description(shortDefinition = "This is a search for stuff!") - @Search - public List findDiagnosticReportsByPatient(@RequiredParam(name = DiagnosticReport.SP_SUBJECT + '.' + Patient.SP_IDENTIFIER) TokenParam thePatientId, - @OptionalParam(name = DiagnosticReport.SP_CODE) TokenOrListParam theNames, @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange, - @IncludeParam(allow = {"DiagnosticReport.result"}) Set theIncludes) throws Exception { - return null; - } + @SuppressWarnings("unused") + public static class SearchProviderWithWhitelist { - } + @Search(type = Patient.class) + public Patient findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist = {"foo", + "bar"}) ReferenceAndListParam theIdentifier) { + return null; + } - @SuppressWarnings("unused") - public static class ReadProvider { + } - @Search(type = Patient.class) - public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { - return null; - } + @SuppressWarnings("unused") + public static class SearchProviderWithListNoType implements IResourceProvider { - @Read(version = false) - public Patient readPatient(@IdParam IdType theId) { - return null; - } + @Override + public Class getResourceType() { + return Patient.class; + } - } - @SuppressWarnings("unused") - public static class SearchProvider { + @Search() + public List findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { + return null; + } - @Search(type = Patient.class) - public Patient findPatient1(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { - return null; - } + } - @Search(type = Patient.class) - public Patient findPatient2( - @Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = {Patient.class}) ReferenceAndListParam theLink) { - return null; - } + @SuppressWarnings("unused") + public static class SearchProviderWithListWithType implements IResourceProvider { - } + @Override + public Class getResourceType() { + return Patient.class; + } - @SuppressWarnings("unused") - public static class SearchProviderWithWhitelist { - @Search(type = Patient.class) - public Patient findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist = {"foo", - "bar"}) ReferenceAndListParam theIdentifier) { - return null; - } + @Search(type = Patient.class) + public List findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { + return null; + } - } + } - @SuppressWarnings("unused") - public static class SearchProviderWithListNoType implements IResourceProvider { + public static class SystemHistoryProvider { - @Override - public Class getResourceType() { - return Patient.class; - } + @History + public List history() { + return null; + } + } - @Search() - public List findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { - return null; - } + public static class TypeHistoryProvider implements IResourceProvider { - } + @Override + public Class getResourceType() { + return Patient.class; + } - @SuppressWarnings("unused") - public static class SearchProviderWithListWithType implements IResourceProvider { + @History + public List history() { + return null; + } - @Override - public Class getResourceType() { - return Patient.class; - } + } + @SuppressWarnings("unused") + public static class VreadProvider { - @Search(type = Patient.class) - public List findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) { - return null; - } + @Search(type = Patient.class) + public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { + return null; + } - } + @Read(version = true) + public Patient readPatient(@IdParam IdType theId) { + return null; + } - public static class SystemHistoryProvider { + } - @History - public List history() { - return null; - } + public static class TypeLevelOperationProvider implements IResourceProvider { - } + public static final String OPERATION_NAME = "op"; - public static class TypeHistoryProvider implements IResourceProvider { + @Operation(name = OPERATION_NAME, idempotent = true) + public IBundleProvider op() { + return null; + } - @Override - public Class getResourceType() { - return Patient.class; - } + @Override + public Class getResourceType() { + return Patient.class; + } - @History - public List history() { - return null; - } + } - } + public static class NamedQueryPlainProvider { - @SuppressWarnings("unused") - public static class VreadProvider { + public static final String QUERY_NAME = "testQuery"; + public static final String DESCRIPTION = "A query description"; + public static final String SP_QUANTITY = "quantity"; - @Search(type = Patient.class) - public Patient findPatient(@Description(shortDefinition = "The patient's identifier (MRN or other card number)") @RequiredParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier) { - return null; - } + @Search(queryName = QUERY_NAME, type = Patient.class) + @Description(formalDefinition = DESCRIPTION) + public Bundle findAllGivenParameter(@RequiredParam(name = SP_QUANTITY) QuantityParam quantity) { + return null; + } + } - @Read(version = true) - public Patient readPatient(@IdParam IdType theId) { - return null; - } + public static class NamedQueryResourceProvider implements IResourceProvider { - } + public static final String QUERY_NAME = "testQuery"; + public static final String SP_PARAM = "param"; - public static class TypeLevelOperationProvider implements IResourceProvider { + @Override + public Class getResourceType() { + return Patient.class; + } - public static final String OPERATION_NAME = "op"; + @Search(queryName = QUERY_NAME) + public Bundle findAllGivenParameter(@OptionalParam(name = SP_PARAM) StringParam param) { + return null; + } - @Operation(name = OPERATION_NAME, idempotent = true) - public IBundleProvider op() { - return null; - } + } - @Override - public Class getResourceType() { - return Patient.class; - } + public static class ProfiledPatientProvider implements IResourceProvider { - } + @Override + public Class getResourceType() { + return PatientSubSub2.class; + } - public static class NamedQueryPlainProvider { + @Search + public List find() { + return null; + } + } - public static final String QUERY_NAME = "testQuery"; - public static final String DESCRIPTION = "A query description"; - public static final String SP_QUANTITY = "quantity"; + public static class MultipleProfilesPatientProvider implements IResourceProvider { - @Search(queryName = QUERY_NAME) - @Description(formalDefinition = DESCRIPTION) - public Bundle findAllGivenParameter(@RequiredParam(name = SP_QUANTITY) QuantityParam quantity) { - return null; - } - } + @Override + public Class getResourceType() { + return PatientSubSub.class; + } - public static class NamedQueryResourceProvider implements IResourceProvider { + @Read(type = PatientTripleSub.class) + public PatientTripleSub read(@IdParam IdType theId) { + return null; + } - public static final String QUERY_NAME = "testQuery"; - public static final String SP_PARAM = "param"; + } - @Override - public Class getResourceType() { - return Patient.class; - } + @ResourceDef(id = PATIENT_SUB) + public static class PatientSub extends Patient { + } - @Search(queryName = QUERY_NAME) - public Bundle findAllGivenParameter(@OptionalParam(name = SP_PARAM) StringParam param) { - return null; - } + @ResourceDef(id = PATIENT_SUB_SUB) + public static class PatientSubSub extends PatientSub { + } - } + @ResourceDef(id = PATIENT_SUB_SUB_2) + public static class PatientSubSub2 extends PatientSub { + } - public static class ProfiledPatientProvider implements IResourceProvider { + @ResourceDef(id = PATIENT_TRIPLE_SUB) + public static class PatientTripleSub extends PatientSubSub { + } - @Override - public Class getResourceType() { - return PatientSubSub2.class; - } + private static Set toStrings(Collection theType) { + HashSet retVal = new HashSet(); + for (IPrimitiveType next : theType) { + retVal.add(next.getValueAsString()); + } + return retVal; + } - @Search - public List find() { - return null; - } - } - - public static class MultipleProfilesPatientProvider implements IResourceProvider { - - @Override - public Class getResourceType() { - return PatientSubSub.class; - } - - @Read(type = PatientTripleSub.class) - public PatientTripleSub read(@IdParam IdType theId) { - return null; - } - - } - - @ResourceDef(id = PATIENT_SUB) - public static class PatientSub extends Patient { - } - - @ResourceDef(id = PATIENT_SUB_SUB) - public static class PatientSubSub extends PatientSub { - } - - @ResourceDef(id = PATIENT_SUB_SUB_2) - public static class PatientSubSub2 extends PatientSub { - } - - @ResourceDef(id = PATIENT_TRIPLE_SUB) - public static class PatientTripleSub extends PatientSubSub { - } - - private static Set toStrings(Collection theType) { - HashSet retVal = new HashSet(); - for (IPrimitiveType next : theType) { - retVal.add(next.getValueAsString()); - } - return retVal; - } - - @AfterAll - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } + @AfterAll + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } } diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/SchemaValidationDstu3Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/SchemaValidationDstu3Test.java index a87dd300e02..dcc4b7287f4 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/SchemaValidationDstu3Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/SchemaValidationDstu3Test.java @@ -13,6 +13,10 @@ import static org.junit.jupiter.api.Assertions.assertFalse; public class SchemaValidationDstu3Test { + static { + + } + private static FhirContext ourCtx = FhirContext.forDstu3(); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaValidationDstu3Test.class); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/SchemaValidationR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/SchemaValidationR4Test.java index f3794a85181..1d870ed32ac 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/SchemaValidationR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/SchemaValidationR4Test.java @@ -5,40 +5,62 @@ import ca.uhn.fhir.util.TestUtil; import ca.uhn.fhir.validation.FhirValidator; import ca.uhn.fhir.validation.ValidationResult; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.TransformerFactory; +import javax.xml.xpath.XPathFactory; + +import java.security.CodeSource; +import java.text.MessageFormat; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertFalse; public class SchemaValidationR4Test { - + private static final Logger ourLog = LoggerFactory.getLogger(SchemaValidationR4Test.class); private static FhirContext ourCtx = FhirContext.forDstu3(); - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SchemaValidationR4Test.class); + @BeforeEach + public void before() { + ourLog.info(getJaxpImplementationInfo("DocumentBuilderFactory", DocumentBuilderFactory.newInstance().getClass())); + ourLog.info(getJaxpImplementationInfo("XPathFactory", XPathFactory.newInstance().getClass())); + ourLog.info(getJaxpImplementationInfo("TransformerFactory", TransformerFactory.newInstance().getClass())); + ourLog.info(getJaxpImplementationInfo("SAXParserFactory", SAXParserFactory.newInstance().getClass())); + + // The following code can be used to force the built in schema parser to be used + // System.setProperty("jaxp.debug", "1"); + // System.setProperty("javax.xml.validation.SchemaFactory:http://www.w3.org/2001/XMLSchema", "com.sun.org.apache.xerces.internal.jaxp.validation.XMLSchemaFactory"); + } /** * See #339 - * + *

    * https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing */ @Test public void testXxe() { //@formatter:off String input = - "\n" + - "\n" + - "]>" + - "" + - "" + - "" + - "

    TEXT &xxe; TEXT
    \n" + + "\n" + + "\n" + + "]>" + + "" + + "" + + "" + + "
    TEXT &xxe; TEXT
    \n" + "
    " + - "
    " + - "" + + "
    " + + "" + "
    " + - ""; + ""; //@formatter:on FhirValidator val = ourCtx.newValidator(); @@ -49,11 +71,26 @@ public class SchemaValidationR4Test { String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.toOperationOutcome()); ourLog.info(encoded); + /* + * If this starts failing, check if xerces (or another simiar library) has slipped in + * to the classpath as a dependency. The logs in the @Before method should include this: + * DocumentBuilderFactory implementation: com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl loaded from: Java Runtime + */ + assertFalse(result.isSuccessful()); assertThat(encoded, containsString("passwd")); assertThat(encoded, containsString("accessExternalDTD")); } + private static String getJaxpImplementationInfo(String componentName, Class componentClass) { + CodeSource source = componentClass.getProtectionDomain().getCodeSource(); + return MessageFormat.format( + "{0} implementation: {1} loaded from: {2}", + componentName, + componentClass.getName(), + source == null ? "Java Runtime" : source.getLocation()); + } + @AfterAll public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-tinder-plugin/pom.xml b/hapi-tinder-plugin/pom.xml index 1da19fdaf24..6be3a8a978f 100644 --- a/hapi-tinder-plugin/pom.xml +++ b/hapi-tinder-plugin/pom.xml @@ -5,7 +5,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../pom.xml @@ -58,37 +58,37 @@ ca.uhn.hapi.fhir hapi-fhir-structures-dstu3 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-hl7org-dstu2 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-r4 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-structures-r5 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu2 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-dstu3 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ca.uhn.hapi.fhir hapi-fhir-validation-resources-r4 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT org.apache.velocity diff --git a/hapi-tinder-test/pom.xml b/hapi-tinder-test/pom.xml index 2f98895c12d..247b0328ab6 100644 --- a/hapi-tinder-test/pom.xml +++ b/hapi-tinder-test/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 109923b7ecc..91128db643f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir pom - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT HAPI-FHIR An open-source implementation of the FHIR specification in Java. https://hapifhir.io @@ -786,6 +786,7 @@ 9.4.39.v20210325 3.0.2 5.7.1 + 0.50.40 6.5.4 5.4.30.Final 6.0.2.Final @@ -962,6 +963,21 @@ javax.mail 1.6.1 + + com.vladsch.flexmark + flexmark + ${flexmark_version} + + + com.vladsch.flexmark + flexmark-ext-tables + ${flexmark_version} + + + com.vladsch.flexmark + flexmark-profile-pegdown + ${flexmark_version} + commons-cli commons-cli @@ -1107,13 +1123,28 @@ io.swagger swagger-annotations - 1.6.1 + 1.6.2 + + + io.swagger.core.v3 + swagger-models + 2.1.7 + + + io.swagger.core.v3 + swagger-core + 2.1.7 mysql mysql-connector-java 8.0.20 + + net.sourceforge.htmlunit + htmlunit + 2.49.1 + net.sf.json-lib json-lib @@ -1517,6 +1548,13 @@ javassist 3.22.0-GA + + org.junit + junit-bom + ${junit_version} + pom + import + org.junit.jupiter junit-jupiter @@ -1544,7 +1582,7 @@ org.junit-pioneer junit-pioneer - 1.1.0 + 1.3.8 org.mariadb.jdbc @@ -1737,6 +1775,11 @@ popper.js 1.16.1 + + org.webjars + swagger-ui + 3.46.0 + org.xmlunit xmlunit-core @@ -1745,19 +1788,19 @@ org.testcontainers testcontainers - 1.15.1 + 1.15.3 test org.testcontainers elasticsearch - 1.15.1 + 1.15.3 test org.testcontainers junit-jupiter - 1.15.1 + 1.15.3 test @@ -1923,7 +1966,7 @@ random @{argLine} ${surefire_jvm_args} 1.0C - false + true @@ -2101,7 +2144,7 @@ com.puppycrawl.tools checkstyle - 8.29 + 8.41.1 @@ -2188,6 +2231,9 @@ + + + @@ -2593,6 +2639,7 @@ hapi-fhir-client hapi-fhir-server hapi-fhir-server-mdm + hapi-fhir-server-openapi hapi-fhir-converter hapi-fhir-validation diff --git a/restful-server-example/pom.xml b/restful-server-example/pom.xml index ee4e9459316..dfb8121f1a7 100644 --- a/restful-server-example/pom.xml +++ b/restful-server-example/pom.xml @@ -8,7 +8,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../pom.xml diff --git a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml index 757146f0f1c..5308513a49d 100644 --- a/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml +++ b/tests/hapi-fhir-base-test-jaxrsserver-kotlin/pom.xml @@ -6,7 +6,7 @@ ca.uhn.hapi.fhir hapi-fhir - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-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 e295bb2a92c..4c39a242c15 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-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 bde4aa9378f..429c414d48a 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 - 5.4.0-PRE7-SNAPSHOT + 5.4.0-PRE8-SNAPSHOT ../../pom.xml From 987264b2717fbcda5f410610e9ca53894194a68c Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sun, 25 Apr 2021 17:38:04 -0400 Subject: [PATCH 38/39] Version bump JENA --- .../resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml | 1 + pom.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml index 928093b5dd3..c9053c1890b 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/changes.yaml @@ -11,6 +11,7 @@
  • Guava (Core): 30.1-jre -> 30.1.1-jre
  • Jackson (Core): 2.12.1 -> 2.12.3
  • Woodstox (Core): 6.2.3 -> 6.2.5
  • +
  • Apache Jena (Core/RDF): 3.16.0 -> 3.17.0
  • Gson (JPA): 2.8.5 -> 2.8.6
  • Caffeine (JPA): 2.7.0 -> 3.0.1
  • Hibernate (JPA): 5.4.26.Final -> 5.4.30.Final
  • diff --git a/pom.xml b/pom.xml index 91128db643f..38b66b8cffa 100644 --- a/pom.xml +++ b/pom.xml @@ -780,7 +780,7 @@ 2.3.1 2.3.0.1 3.0.0 - 3.16.0 + 3.17.0 3.0.0 9.4.39.v20210325 From 0246e7dd0eafcb3c6c6dbcac9bd167c3894d3652 Mon Sep 17 00:00:00 2001 From: Kevin Dougan SmileCDR <72025369+KevinDougan-SmileCDR@users.noreply.github.com> Date: Mon, 26 Apr 2021 14:13:42 -0400 Subject: [PATCH 39/39] =?UTF-8?q?2572=20-=20Add=20master=20Branch=20change?= =?UTF-8?q?s=20related=20to=20backport=20of=20fix=20for=20#2171=E2=80=A6?= =?UTF-8?q?=20(#2574)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 2572 - Add master Branch changes related to backport of fix for #2171 for new 5.3.3 release. * 2572 - Added some missing changelog folders for the v5.3.1 and v5.3.2 releases. * 2572 - Added a missing backport changelog entry for the v5.3.1 release. * 2572 - Added a missing VersionEnum entry for the V5_3_1 release. --- hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java | 2 ++ .../resources/ca/uhn/hapi/fhir/changelog/5_3_1/version.yaml | 3 +++ .../resources/ca/uhn/hapi/fhir/changelog/5_3_2/version.yaml | 3 +++ .../resources/ca/uhn/hapi/fhir/changelog/5_3_3/version.yaml | 3 +++ .../5_4_0/2407-allow-partition-date-for-nonpartitionable.yaml | 1 + .../fhir/changelog/5_4_0/2417-avoid-npe-indexing-timing.yaml | 1 + 6 files changed, 13 insertions(+) create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_1/version.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_2/version.yaml create mode 100644 hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_3/version.yaml diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java index 773cc30bd26..903808720d9 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/VersionEnum.java @@ -68,7 +68,9 @@ public enum VersionEnum { V5_2_0, V5_2_1, V5_3_0, + V5_3_1, V5_3_2, + V5_3_3, V5_4_0, ; diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_1/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_1/version.yaml new file mode 100644 index 00000000000..63b20bb11c0 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_1/version.yaml @@ -0,0 +1,3 @@ +--- +release-date: "2021-03-11" +codename: "Odyssey" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_2/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_2/version.yaml new file mode 100644 index 00000000000..2e81becc0e4 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_2/version.yaml @@ -0,0 +1,3 @@ +--- +release-date: "2021-04-14" +codename: "Odyssey" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_3/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_3/version.yaml new file mode 100644 index 00000000000..51a63b4dd11 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_3_3/version.yaml @@ -0,0 +1,3 @@ +--- +release-date: "2021-04-26" +codename: "Odyssey" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2407-allow-partition-date-for-nonpartitionable.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2407-allow-partition-date-for-nonpartitionable.yaml index b8942a6f558..61e73a7fc9f 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2407-allow-partition-date-for-nonpartitionable.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2407-allow-partition-date-for-nonpartitionable.yaml @@ -4,3 +4,4 @@ issue: 2407 title: "When using the JPA server in partitioned mode with a partition interceptor, the interceptor is now called even for resource types that can not be placed in a non-default partition (e.g. SearchParameter, CodeSystem, etc.). The interceptor may return null or default in this case, but can include a non-null partition date if needed." +backport: 5.3.1 diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2417-avoid-npe-indexing-timing.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2417-avoid-npe-indexing-timing.yaml index c20daca2cca..619928991f6 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2417-avoid-npe-indexing-timing.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/5_4_0/2417-avoid-npe-indexing-timing.yaml @@ -3,3 +3,4 @@ type: fix issue: 2417 title: "A NullPointerException was corrected when indexing resources containing an indexed Period field that had a start but not an end defined." +backport: 5.3.3