diff --git a/.github/workflows/cdr_check/pyproject.toml b/.github/workflows/cdr_check/pyproject.toml index 1743923e4294..3e42b39ae12e 100644 --- a/.github/workflows/cdr_check/pyproject.toml +++ b/.github/workflows/cdr_check/pyproject.toml @@ -3,6 +3,7 @@ name = "tester" version = "0.1.0" description = "" authors = ["Tadgh "] +package-mode = false [tool.poetry.dependencies] python = "^3.10" diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java index fcc1c7a009a9..f524aefc71a8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirContext.java @@ -137,6 +137,15 @@ public class FhirContext { private volatile Boolean myFormatNDJsonSupported; private volatile Boolean myFormatRdfSupported; private IFhirValidatorFactory myFhirValidatorFactory = FhirValidator::new; + /** + * If true, parsed resources will have the json string + * used to create them stored + * in the UserData. + * + * This is to help with validation, because the parser itself is far + * more lenient than validation might be. + */ + private boolean myStoreResourceJsonFlag = false; /** * @deprecated It is recommended that you use one of the static initializer methods instead @@ -762,6 +771,14 @@ public void setValidationSupport(IValidationSupport theValidationSupport) { myValidationSupport = theValidationSupport; } + public void setStoreRawJson(boolean theStoreResourceJsonFlag) { + myStoreResourceJsonFlag = theStoreResourceJsonFlag; + } + + public boolean isStoreResourceJson() { + return myStoreResourceJsonFlag; + } + public IFhirVersion getVersion() { return myVersion; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.java index ce41d76c1aa6..b6eea3fce065 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.java @@ -66,6 +66,10 @@ public CustomThymeleafNarrativeGenerator(List theNarrativePropertyFiles) this(theNarrativePropertyFiles.toArray(new String[0])); } + public CustomThymeleafNarrativeGenerator(CustomThymeleafNarrativeGenerator theNarrativeGenerator) { + setManifest(theNarrativeGenerator.getManifest()); + } + @Override public NarrativeTemplateManifest getManifest() { NarrativeTemplateManifest retVal = myManifest; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java index 10ef4b465750..e65c53519b56 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java @@ -46,6 +46,7 @@ import ca.uhn.fhir.util.CollectionUtil; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.MetaUtil; +import ca.uhn.fhir.util.ResourceUtil; import ca.uhn.fhir.util.UrlUtil; import com.google.common.base.Charsets; import jakarta.annotation.Nullable; @@ -640,12 +641,30 @@ public T parseResource(Class theResourceType, Reade * We do this so that the context can verify that the structure is for * the correct FHIR version */ + Reader readerToUse = theReader; if (theResourceType != null) { myContext.getResourceDefinition(theResourceType); + if (myContext.isStoreResourceJson()) { + readerToUse = new PreserveStringReader(theReader); + } } // Actually do the parse - T retVal = doParseResource(theResourceType, theReader); + T retVal = doParseResource(theResourceType, readerToUse); + + if (theResourceType != null && myContext.isStoreResourceJson()) { + PreserveStringReader psr = (PreserveStringReader) readerToUse; + if (psr.hasString()) { + try { + ResourceUtil.addRawDataToResource(retVal, getEncoding(), psr.toString()); + psr.close(); + } catch (IOException ex) { + ourLog.warn( + "Unable to store raw JSON on resource. This won't affect functionality, but validation will use the resource itself, which may result in different validation results than a $validation operation.", + ex); + } + } + } RuntimeResourceDefinition def = myContext.getResourceDefinition(retVal); if ("Bundle".equals(def.getName())) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/PreserveStringReader.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/PreserveStringReader.java new file mode 100644 index 000000000000..b10d4350f3cc --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/PreserveStringReader.java @@ -0,0 +1,44 @@ +package ca.uhn.fhir.parser; + +import jakarta.annotation.Nonnull; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringWriter; + +public class PreserveStringReader extends Reader { + + private final Reader myReader; + + private final StringWriter myWriter; + + public PreserveStringReader(Reader theReader) { + super(theReader); + myReader = theReader; + myWriter = new StringWriter(); + } + + @Override + public int read(@Nonnull char[] theBuffer, int theOffset, int theLength) throws IOException { + int out = myReader.read(theBuffer, theOffset, theLength); + if (out >= 0) { + myWriter.write(theBuffer, theOffset, out); + } + + return out; + } + + @Override + public void close() throws IOException { + myReader.close(); + myWriter.close(); + } + + public boolean hasString() { + return myWriter.getBuffer().length() > 0; + } + + public String toString() { + return myWriter.toString(); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ResourceUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ResourceUtil.java index aff515dc3d1e..21d62dc315ba 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ResourceUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ResourceUtil.java @@ -22,11 +22,20 @@ import ca.uhn.fhir.context.BaseRuntimeChildDefinition; import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.EncodingEnum; +import jakarta.annotation.Nonnull; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseResource; +import java.io.IOException; + public class ResourceUtil { + private static final String ENCODING = "ENCODING_TYPE"; + private static final String RAW_ = "RAW_%s"; + + private ResourceUtil() {} + /** * This method removes the narrative from the resource, or if the resource is a bundle, removes the narrative from * all of the resources in the bundle @@ -47,4 +56,27 @@ public static void removeNarrative(FhirContext theContext, IBaseResource theInpu textElement.getMutator().setValue(theInput, null); } } + + public static void addRawDataToResource( + @Nonnull IBaseResource theResource, @Nonnull EncodingEnum theEncodingType, String theSerializedData) + throws IOException { + theResource.setUserData(getRawUserDataKey(theEncodingType), theSerializedData); + theResource.setUserData(ENCODING, theEncodingType); + } + + public static EncodingEnum getEncodingTypeFromUserData(@Nonnull IBaseResource theResource) { + return (EncodingEnum) theResource.getUserData(ENCODING); + } + + public static String getRawStringFromResourceOrNull(@Nonnull IBaseResource theResource) { + EncodingEnum type = (EncodingEnum) theResource.getUserData(ENCODING); + if (type != null) { + return (String) theResource.getUserData(getRawUserDataKey(type)); + } + return null; + } + + private static String getRawUserDataKey(EncodingEnum theEncodingEnum) { + return String.format(RAW_, theEncodingEnum.name()); + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/ValidationContext.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/ValidationContext.java index 5a454fe477ee..35e49d72653b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/ValidationContext.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/validation/ValidationContext.java @@ -26,7 +26,9 @@ import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.util.ObjectUtil; +import ca.uhn.fhir.util.ResourceUtil; import jakarta.annotation.Nonnull; +import org.apache.commons.lang3.ObjectUtils; import org.hl7.fhir.instance.model.api.IBaseResource; import java.util.ArrayList; @@ -103,12 +105,17 @@ public static IValidationContext forResource( IEncoder encoder = new IEncoder() { @Override public String encode() { - return theContext.newJsonParser().encodeResourceToString(theResource); + // use the stored json string, if available + // otherwise, encode the actual resource + return ObjectUtils.firstNonNull( + ResourceUtil.getRawStringFromResourceOrNull(theResource), + theContext.newJsonParser().encodeResourceToString(theResource)); } @Override public EncodingEnum getEncoding() { - return EncodingEnum.JSON; + return ObjectUtils.defaultIfNull( + ResourceUtil.getEncodingTypeFromUserData(theResource), EncodingEnum.JSON); } }; return new ValidationContext<>(theContext, theResource, encoder, options); diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/.keep b/hapi-fhir-base/src/test/java/ca/uhn/fhir/parser/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/version.yaml index 2731145668e1..1ae495aea4c8 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/version.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_0/version.yaml @@ -1,3 +1,3 @@ --- release-date: "2024-11-15" -codename: "TBD" +codename: "Despina" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/6424-change-structure-definitions-to-never-expire-in-cache-during-validation.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/6424-change-structure-definitions-to-never-expire-in-cache-during-validation.yaml new file mode 100644 index 000000000000..7a15c3532bb0 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/6424-change-structure-definitions-to-never-expire-in-cache-during-validation.yaml @@ -0,0 +1,5 @@ +--- +type: change +issue: 6424 +title: "Changed VersionSpecificWorkerContextWrapper to never expire StructureDefinition entries in the cache, +which is needed because the validator makes assumptions about StructureDefinitions never changing." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/6538-improve-transaction-performance.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/6538-improve-transaction-performance.yaml new file mode 100644 index 000000000000..705b36ee6596 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/6538-improve-transaction-performance.yaml @@ -0,0 +1,7 @@ +--- +type: perf +issue: 6538 +title: "In HAPI FHIR 8.0.0, transaction processing has been significantly improved thanks + to ticket [#6460](https://github.com/hapifhir/hapi-fhir/pull/6460). This enhancement + has been partially backported to the 7.6.x release line in order to provide partial improvement + prior to the release of HAPI FHIR 8.0.0." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/6538-improve-validation-performance.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/6538-improve-validation-performance.yaml new file mode 100644 index 000000000000..2e85cd2cd520 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/6538-improve-validation-performance.yaml @@ -0,0 +1,7 @@ +--- +type: perf +issue: 6538 +title: "In HAPI FHIR 8.0.0, validation processing has been significantly improved thanks + to ticket [#6508](https://github.com/hapifhir/hapi-fhir/pull/6508). This enhancement + has been partially backported to the 7.6.x release line in order to provide partial improvement + prior to the release of HAPI FHIR 8.0.0." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/upgrade.md b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/upgrade.md new file mode 100644 index 000000000000..120e264044d4 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/upgrade.md @@ -0,0 +1,12 @@ +## Device membership in Patient Compartment + +As of 7.6.1, versions of FHIR below R5 now consider the `Device` resource's `patient` Search Parameter to be in the Patient Compartment. The following features are affected: + +- Patient Search with `_revInclude=*` +- Patient instance-level `$everything` operation +- Patient type-level `$everything` operation +- Automatic Search Narrowing +- Bulk Export + +Previously, there were various shims in the code that permitted similar behaviour in these features. Those shims have been removed. The only remaining component is [Advanced Compartment Authorization](/hapi-fhir/docs/security/authorization_interceptor.html#advanced-compartment-authorization), which can still be used +to add other Search Parameters into a given compartment. diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/version.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/version.yaml new file mode 100644 index 000000000000..5ea269a1c9a3 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_6_1/version.yaml @@ -0,0 +1,3 @@ +--- +release-date: "2024-12-18" +codename: "Despina" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6536-make-device-part-of-patient-compartment.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6536-make-device-part-of-patient-compartment.yaml index cb51d174f77e..3fe4f5f8104e 100644 --- a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6536-make-device-part-of-patient-compartment.yaml +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6536-make-device-part-of-patient-compartment.yaml @@ -1,6 +1,7 @@ --- type: add jira: SMILE-9260 +backport: 7.6.1 title: "The `patient` search parameter for the `Device` resource has been added to the Patient Compartment for the purposes of: - AuthorizationInterceptor diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6574-storing-raw-json-on-resource-parsing.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6574-storing-raw-json-on-resource-parsing.yaml new file mode 100644 index 000000000000..9eb375a460fe --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6574-storing-raw-json-on-resource-parsing.yaml @@ -0,0 +1,10 @@ +--- +type: fix +issue: 6574 +jira: SMILE-9432 +title: "The raw json of parsed resources will be kept in the UserData + (key: `RAW_JSON`) of the resource itself. + This is to allow consistency in handling validation downstream, + since otherwise the FhirParser is far more lenient about what + it can parse than $validate is for what it accepts. +" diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6580-migration-result-column.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6580-migration-result-column.yaml new file mode 100644 index 000000000000..57ef9d89ca65 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6580-migration-result-column.yaml @@ -0,0 +1,7 @@ +--- +type: add +issue: 6580 +title: "A new `RESULT` column has been added to the database migration table to record the migration execution result. +values are `NOT_APPLIED_SKIPPED` (either skipped via the `skip-versions` flag or if the migration task was stubbed), +`NOT_APPLIED_NOT_FOR_THIS_DATABASE` (does not apply to that database), `NOT_APPLIED_PRECONDITION_NOT_MET` (not run based on a SQL script outcome), +`NOT_APPLIED_ALLOWED_FAILURE` (the migration failed, but it is permitted to fail), `APPLIED`." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6587-enhance-vital-signs-narrative-generation-template.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6587-enhance-vital-signs-narrative-generation-template.yaml new file mode 100644 index 000000000000..41531e5d2397 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6587-enhance-vital-signs-narrative-generation-template.yaml @@ -0,0 +1,5 @@ +--- +type: add +issue: 6587 +title: "Enhanced the IPS vital signs narrative template to include code and value information for + all entries in the `Observation.component` property." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6589_optimize_transactions_in_mass_ingestion_mode b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6589_optimize_transactions_in_mass_ingestion_mode new file mode 100644 index 000000000000..871d01991bad --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_8_0/6589_optimize_transactions_in_mass_ingestion_mode @@ -0,0 +1,4 @@ +--- +type: perf +issue: 6589 +title: "When performing data loading into a JPA repository using FHIR transactions with Mass Ingestion Mode enabled, the prefetch routine has been optimized to avoid loading the current resource body/contents, since these are not actually needed in Mass Ingestion mode. This avoids a redundant select statement being issued for each transaction and should improve performance." diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java index d1e925ee1775..77fc2b868251 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/ValidationSupportConfig.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.DaoRegistry; import ca.uhn.fhir.jpa.dao.JpaPersistedResourceValidationSupport; +import ca.uhn.fhir.jpa.validation.FhirContextValidationSupportSvc; import ca.uhn.fhir.jpa.validation.ValidatorPolicyAdvisor; import ca.uhn.fhir.jpa.validation.ValidatorResourceFetcher; import ca.uhn.fhir.validation.IInstanceValidatorModule; @@ -42,6 +43,11 @@ public class ValidationSupportConfig { @Autowired private FhirContext myFhirContext; + @Bean + public FhirContextValidationSupportSvc fhirValidationSupportSvc() { + return new FhirContextValidationSupportSvc(myFhirContext); + } + @Bean(name = JpaConfig.DEFAULT_PROFILE_VALIDATION_SUPPORT) public DefaultProfileValidationSupport defaultProfileValidationSupport() { return new DefaultProfileValidationSupport(myFhirContext); 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 dd121e4459eb..1a0ff20261ac 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 @@ -206,12 +206,42 @@ public

void preFetchResources( * However, for realistic average workloads, this should reduce the number of round trips. */ if (!idChunk.isEmpty()) { - List entityChunk = prefetchResourceTableAndHistory(idChunk); + List entityChunk = null; + + /* + * Unless we're in Mass Ingestion mode, we will pre-fetch the current + * saved resource text in HFJ_RES_VER (ResourceHistoryTable). If we're + * in Mass Ingestion Mode, we don't need to do that because every update + * will generate a new version anyway so the system never needs to know + * the current contents. + */ + if (!myStorageSettings.isMassIngestionMode()) { + entityChunk = prefetchResourceTableAndHistory(idChunk); + } if (thePreFetchIndexes) { + /* + * If we're in mass ingestion mode, then we still need to load the resource + * entries in HFJ_RESOURCE (ResourceTable). We combine that with the search + * for tokens (since token is the most likely kind of index to be populated + * for any arbitrary resource type). + * + * For all other index types, we only load indexes if at least one + * HFJ_RESOURCE row indicates that a resource we care about actually has + * index rows of the given type. + */ + if (entityChunk == null) { + String jqlQuery = + "SELECT r FROM ResourceTable r LEFT JOIN FETCH r.myParamsToken WHERE r.myPid IN ( :IDS )"; + TypedQuery query = myEntityManager.createQuery(jqlQuery, ResourceTable.class); + query.setParameter("IDS", idChunk); + entityChunk = query.getResultList(); + } else { + prefetchByField("token", "myParamsToken", ResourceTable::isParamsTokenPopulated, entityChunk); + } + prefetchByField("string", "myParamsString", ResourceTable::isParamsStringPopulated, entityChunk); - prefetchByField("token", "myParamsToken", ResourceTable::isParamsTokenPopulated, entityChunk); prefetchByField("date", "myParamsDate", ResourceTable::isParamsDatePopulated, entityChunk); prefetchByField( "quantity", "myParamsQuantity", ResourceTable::isParamsQuantityPopulated, entityChunk); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java index 6312c7ba085a..16837edded6d 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java @@ -173,7 +173,7 @@ protected EntriesToProcessMap doTransactionWriteOperations( * is for fast writing of data. * * Note that it's probably not necessary to reset it back, it should - * automatically go back to the default value after the transaction but + * automatically go back to the default value after the transaction, but * we reset it just to be safe. */ FlushModeType initialFlushMode = myEntityManager.getFlushMode(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/FhirContextValidationSupportSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/FhirContextValidationSupportSvc.java new file mode 100644 index 000000000000..eb044eff6c6d --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/FhirContextValidationSupportSvc.java @@ -0,0 +1,19 @@ +package ca.uhn.fhir.jpa.validation; + +import ca.uhn.fhir.context.FhirContext; + +/** + * This bean will set our context to store the raw json of parsed + * resources when they come in for creation. + * - + * This is done so we can ensure that validation is done correctly, + * and the JSONParser is far more lenient than our validators. + * - + * See {@link FhirContext#isStoreResourceJson()} + */ +public class FhirContextValidationSupportSvc { + + public FhirContextValidationSupportSvc(FhirContext theContext) { + theContext.setStoreRawJson(true); + } +} diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html index 55f3d14dfc92..b5162fff7a44 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/allergyintolerance.html @@ -31,16 +31,7 @@

Allergies And Intolerances
Reaction Severity Comments - - - Onset - - - Onset - - - - + Date diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html index 9e4bfcc6a965..c53130d02cd2 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/historyofprocedures.html @@ -17,11 +17,11 @@
History Of Procedures
- - + + Procedure Comments - Date + Date diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html index e21ca2e1ae24..4a48c5ad4e12 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/immunizations.html @@ -25,15 +25,15 @@
Immunizations
- - + + Immunization Status Comments - Manufacturer + Manufacturer Lot Number Comments - Date + Date diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html index ed4a8f128d75..016e13a074e6 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/medicationsummary.html @@ -35,12 +35,12 @@
Medication Summary: Medication Requests
- - + th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/narrativeLink')}"> + + Medication - Status + Status Route @@ -48,7 +48,7 @@
Medication Summary: Medication Requests
Sig Comments - Authored Date + Authored Date
@@ -74,15 +74,15 @@
Medication Summary: Medication Statements
- - + th:with="extension=${entry.getResource().getExtensionByUrl('http://hl7.org/fhir/StructureDefinition/narrativeLink')}"> + + Medication - Status + Status Route Sig - Date + Date diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html index 11f06d54bf52..1accc7009a1c 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/pasthistoryofillness.html @@ -19,12 +19,12 @@
Past History of Illnesses
- - + + Medical Problem Status Comments - Onset Date + Onset Date diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html index 5c0e796bd5c9..b2685593ef11 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/problemlist.html @@ -19,12 +19,12 @@
Problem List
- - + + Medical Problems Status Comments - Onset Date + Onset Date diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/utility-fragments.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/utility-fragments.html index 3dea18d43a85..4d02950e2854 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/utility-fragments.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/utility-fragments.html @@ -22,21 +22,33 @@ + + + + + Org Name + + + + + - - - Medication - - - Medication - - + + + + Medication + + + Medication + + + - + Medication @@ -94,49 +106,23 @@ - - - - Date - - Date - - Date - Date - - Date - - - - - - + + - Date + Date + th:text="*{#strings.concatReplaceNulls('', getStartElement().getValueAsString(), ' - ', getEndElement().getValueAsString() )}"> Date Date Date + th:text="*{#strings.concatReplaceNulls('', getLow().getValueAsString(), ' - ', getHigh().getValueAsString() )}">Date Date - - - - Date - Date - - - - @@ -247,3 +233,18 @@ + + + + + Display + Code + Value + Value + Value + + + + + + diff --git a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html index f890ad6c26e1..55391cee2eda 100644 --- a/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html +++ b/hapi-fhir-jpaserver-ips/src/main/resources/ca/uhn/fhir/jpa/ips/narrative/vitalsigns.html @@ -4,6 +4,7 @@ Result: Observation.valueQuantity || Observation.valueDateTime || Observation.valueCodeableConcept.text || Observation.valueCodeableConcept.coding[x].display (separated by
) || Observation.valueString Unit: Observation.valueQuantity.unit Interpretation: Observation.interpretation[0].text || Observation.interpretation[0].coding[x].display (separated by
) +Component(s): Observation.component[x].display || Observation.component[x].code + Observation.component[x].value (items separated by comma) Comments: Observation.note[x].text (separated by
) Date: Observation.effectiveDateTime || Observation.effectivePeriod.start */--> @@ -16,6 +17,7 @@
Vital Signs
Result Unit Interpretation + Component(s) Comments Date @@ -23,12 +25,13 @@
Vital Signs
- - + + Code Result Unit - Interpretation + Interpretation + Component(s) Comments Date diff --git a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java index 96464a82b4de..80018cb7ff1c 100644 --- a/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java +++ b/hapi-fhir-jpaserver-ips/src/test/java/ca/uhn/fhir/jpa/ips/generator/IpsGeneratorSvcImplTest.java @@ -25,6 +25,8 @@ import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.CarePlan; import org.hl7.fhir.r4.model.ClinicalImpression; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; import org.hl7.fhir.r4.model.Composition; import org.hl7.fhir.r4.model.Condition; import org.hl7.fhir.r4.model.Consent; @@ -45,6 +47,7 @@ import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.PositiveIntType; import org.hl7.fhir.r4.model.Procedure; +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.StringType; @@ -667,6 +670,59 @@ public void testSelectGenerator() { assertSame(strategy2, svc.selectGenerationStrategy("http://2")); } + @Test + public void testVitalSigns_withComponents() throws IOException { + // Setup Patient + initializeGenerationStrategy(); + registerPatientDaoWithRead(); + + Observation observation1 = new Observation(); + observation1.setId("Observation/1"); + observation1.setStatus(Observation.ObservationStatus.FINAL); + observation1.setCode( + new CodeableConcept().addCoding( + new Coding("http://loinc.org", "85354-9", "Blood pressure panel with all children optional") + ) + ); + observation1.addComponent( + new Observation.ObservationComponentComponent( + new CodeableConcept( + new Coding("http://loinc.org", "8480-6", "Systolic blood pressure") + ) + ).setValue(new Quantity().setValue(125).setUnit("mmHg").setSystem("http://unitsofmeasure.org").setCode("mm[Hg]")) + ); + observation1.addComponent( + new Observation.ObservationComponentComponent( + new CodeableConcept( + new Coding("http://loinc.org", "8462-4", "Diastolic blood pressure") + ) + ).setValue(new Quantity().setValue(75).setUnit("mmHg").setSystem("http://unitsofmeasure.org").setCode("mm[Hg]")) + ); + + ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.put(observation1, BundleEntrySearchModeEnum.MATCH); + IFhirResourceDao observationDao = registerResourceDaoWithNoData(Observation.class); + when(observationDao.search(any(), any())).thenReturn(new SimpleBundleProvider(Lists.newArrayList(observation1))); + registerRemainingResourceDaos(); + + // Test + Bundle outcome = (Bundle) mySvc.generateIps(new SystemRequestDetails(), new IdType(PATIENT_ID), null); + + // Verify + Composition compositions = (Composition) outcome.getEntry().get(0).getResource(); + Composition.SectionComponent section = findSection(compositions, DefaultJpaIpsGenerationStrategy.SECTION_CODE_VITAL_SIGNS); + + HtmlPage narrativeHtml = HtmlUtil.parseAsHtml(section.getText().getDivAsString()); + ourLog.info("Narrative:\n{}", narrativeHtml.asXml()); + + DomNodeList tables = narrativeHtml.getElementsByTagName("table"); + assertThat(tables).hasSize(1); + HtmlTable table = (HtmlTable) tables.get(0); + assertEquals("Code", table.getHeader().getRows().get(0).getCell(0).asNormalizedText()); + assertEquals("Blood pressure panel with all children optional", table.getBodies().get(0).getRows().get(0).getCell(0).asNormalizedText()); + assertEquals("Component(s)", table.getHeader().getRows().get(0).getCell(4).asNormalizedText()); + assertEquals("Systolic blood pressure 125 , Diastolic blood pressure 75", table.getBodies().get(0).getRows().get(0).getCell(4).asNormalizedText()); + } + @Nonnull private Composition.SectionComponent findSection(Composition compositions, String theSectionCode) { return compositions diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java index c84f4b5af5e9..b928c6ecf2bb 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4QueryCountTest.java @@ -20,7 +20,6 @@ import ca.uhn.fhir.jpa.api.config.JpaStorageSettings; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.api.dao.ReindexParameters; -import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; import ca.uhn.fhir.jpa.api.model.DeleteMethodOutcome; import ca.uhn.fhir.jpa.api.model.ExpungeOptions; import ca.uhn.fhir.jpa.api.model.HistoryCountModeEnum; @@ -28,6 +27,7 @@ import ca.uhn.fhir.jpa.entity.TermValueSet; import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum; import ca.uhn.fhir.jpa.interceptor.ForceOffsetSearchModeInterceptor; +import ca.uhn.fhir.jpa.model.dao.JpaPid; import ca.uhn.fhir.jpa.model.entity.ResourceTable; import ca.uhn.fhir.jpa.model.util.JpaConstants; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; @@ -80,6 +80,7 @@ import org.hl7.fhir.r4.model.Narrative; import org.hl7.fhir.r4.model.Observation; import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.Parameters; import org.hl7.fhir.r4.model.Patient; import org.hl7.fhir.r4.model.Practitioner; @@ -107,11 +108,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; -import org.springframework.util.comparator.ComparableComparator; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.UUID; @@ -132,7 +133,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** @@ -229,14 +229,14 @@ public void testExpungeAllVersionsWithTagsDeletesRow() { p.getMeta().addTag().setSystem("http://foo").setCode("bar"); p.setActive(true); p.addName().setFamily("FOO"); - myPatientDao.update(p).getId(); + myPatientDao.update(p, mySrd); for (int j = 0; j < 5; j++) { p.setActive(!p.getActive()); - myPatientDao.update(p); + myPatientDao.update(p, mySrd); } - myPatientDao.delete(new IdType("Patient/TEST" + i)); + myPatientDao.delete(new IdType("Patient/TEST" + i), mySrd); } myStorageSettings.setExpungeEnabled(true); @@ -344,7 +344,7 @@ public void testUpdateWithNoChanges() { Patient p = new Patient(); p.addIdentifier().setSystem("urn:system").setValue("2"); p.setManagingOrganization(new Reference(orgId)); - return myPatientDao.create(p).getId().toUnqualified(); + return myPatientDao.create(p, mySrd).getId().toUnqualified(); }); myCaptureQueriesListener.clear(); @@ -353,7 +353,7 @@ public void testUpdateWithNoChanges() { p.setId(id.getIdPart()); p.addIdentifier().setSystem("urn:system").setValue("2"); p.setManagingOrganization(new Reference(orgId)); - myPatientDao.update(p); + myPatientDao.update(p, mySrd); }); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread()).hasSize(4); @@ -375,7 +375,7 @@ public void testUpdateWithChanges() { Patient p = new Patient(); p.addIdentifier().setSystem("urn:system").setValue("2"); p.setManagingOrganization(new Reference(orgId)); - return myPatientDao.create(p).getId().toUnqualified(); + return myPatientDao.create(p, mySrd).getId().toUnqualified(); }); myCaptureQueriesListener.clear(); @@ -384,7 +384,7 @@ public void testUpdateWithChanges() { p.setId(id.getIdPart()); p.addIdentifier().setSystem("urn:system").setValue("3"); p.setManagingOrganization(new Reference(orgId2)); - myPatientDao.update(p).getResource(); + assertNotNull(myPatientDao.update(p, mySrd).getResource()); }); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread()).hasSize(5); @@ -458,19 +458,17 @@ public void testUpdateWithChangesAndTags() { Patient p = new Patient(); p.getMeta().addTag("http://system", "foo", "display"); p.addIdentifier().setSystem("urn:system").setValue("2"); - return myPatientDao.create(p).getId().toUnqualified(); + return myPatientDao.create(p, mySrd).getId().toUnqualified(); }); - runInTransaction(() -> { - assertEquals(1, myResourceTagDao.count()); - }); + runInTransaction(() -> assertEquals(1, myResourceTagDao.count())); myCaptureQueriesListener.clear(); runInTransaction(() -> { Patient p = new Patient(); p.setId(id.getIdPart()); p.addIdentifier().setSystem("urn:system").setValue("3"); - IBaseResource newRes = myPatientDao.update(p).getResource(); + IBaseResource newRes = myPatientDao.update(p, mySrd).getResource(); assertEquals(1, newRes.getMeta().getTag().size()); }); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); @@ -628,12 +626,12 @@ public void testRead() { IIdType id = runInTransaction(() -> { Patient p = new Patient(); p.addIdentifier().setSystem("urn:system").setValue("2"); - return myPatientDao.create(p).getId().toUnqualified(); + return myPatientDao.create(p, mySrd).getId().toUnqualified(); }); myCaptureQueriesListener.clear(); runInTransaction(() -> { - myPatientDao.read(id.toVersionless()); + myPatientDao.read(id.toVersionless(), mySrd); }); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); assertThat(myCaptureQueriesListener.getSelectQueriesForCurrentThread()).hasSize(2); @@ -916,6 +914,7 @@ public void testDeleteMultiple() { assertEquals(10, outcome.getDeletedEntities().size()); } + @SuppressWarnings("unchecked") @Test public void testDeleteExpungeStep() { // Setup @@ -961,7 +960,7 @@ public void testUpdateWithClientAssignedId_DeletesDisabled() { Patient p = new Patient(); p.setId("AAA"); p.getMaritalStatus().setText("123"); - myPatientDao.update(p).getId().toUnqualified(); + myPatientDao.update(p, mySrd).getId().toUnqualified(); }); @@ -972,7 +971,7 @@ public void testUpdateWithClientAssignedId_DeletesDisabled() { Patient p = new Patient(); p.setId("AAA"); p.getMaritalStatus().setText("456"); - myPatientDao.update(p).getId().toUnqualified(); + myPatientDao.update(p, mySrd).getId().toUnqualified(); }); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); @@ -991,7 +990,7 @@ public void testUpdateWithClientAssignedId_DeletesDisabled() { Patient p = new Patient(); p.setId("AAA"); p.getMaritalStatus().setText("789"); - myPatientDao.update(p).getId().toUnqualified(); + myPatientDao.update(p, mySrd).getId().toUnqualified(); }); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); @@ -1043,7 +1042,7 @@ public void testReferenceToForcedId() { myCaptureQueriesListener.clear(); observation = new Observation(); observation.getSubject().setReference("Patient/P"); - myObservationDao.create(observation); + myObservationDao.create(observation, mySrd); // select: lookup forced ID assertEquals(1, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); @@ -1067,7 +1066,7 @@ public void testReferenceToForcedId_DeletesDisabled() { patient.setActive(true); myCaptureQueriesListener.clear(); - myPatientDao.update(patient); + myPatientDao.update(patient, mySrd); /* * Add a resource with a forced ID target link @@ -1076,7 +1075,7 @@ public void testReferenceToForcedId_DeletesDisabled() { myCaptureQueriesListener.clear(); Observation observation = new Observation(); observation.getSubject().setReference("Patient/P"); - myObservationDao.create(observation); + myObservationDao.create(observation, mySrd); myCaptureQueriesListener.logSelectQueries(); assertEquals(0, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); @@ -1092,7 +1091,7 @@ public void testReferenceToForcedId_DeletesDisabled() { myCaptureQueriesListener.clear(); observation = new Observation(); observation.getSubject().setReference("Patient/P"); - myObservationDao.create(observation); + myObservationDao.create(observation, mySrd); // select: no lookups needed because of cache assertEquals(0, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); @@ -1219,16 +1218,16 @@ public void testHistory_Server() { Patient p = new Patient(); p.setId("A"); p.addIdentifier().setSystem("urn:system").setValue("1"); - myPatientDao.update(p).getId().toUnqualified(); + myPatientDao.update(p, mySrd).getId().toUnqualified(); p = new Patient(); p.setId("B"); p.addIdentifier().setSystem("urn:system").setValue("2"); - myPatientDao.update(p).getId().toUnqualified(); + myPatientDao.update(p, mySrd).getId().toUnqualified(); p = new Patient(); p.addIdentifier().setSystem("urn:system").setValue("2"); - myPatientDao.create(p).getId().toUnqualified(); + myPatientDao.create(p, mySrd).getId().toUnqualified(); }); myCaptureQueriesListener.clear(); @@ -1268,8 +1267,7 @@ public void testHistory_Server() { /** * This could definitely stand to be optimized some, since we load tags individually * for each resource - */ - /** + * * See the class javadoc before changing the counts in this test! */ @Test @@ -1282,20 +1280,20 @@ public void testHistory_Server_WithTags() { p.getMeta().addTag("system", "code2", "displaY2"); p.setId("A"); p.addIdentifier().setSystem("urn:system").setValue("1"); - myPatientDao.update(p).getId().toUnqualified(); + myPatientDao.update(p, mySrd).getId().toUnqualified(); p = new Patient(); p.getMeta().addTag("system", "code1", "displaY1"); p.getMeta().addTag("system", "code2", "displaY2"); p.setId("B"); p.addIdentifier().setSystem("urn:system").setValue("2"); - myPatientDao.update(p).getId().toUnqualified(); + myPatientDao.update(p, mySrd).getId().toUnqualified(); p = new Patient(); p.getMeta().addTag("system", "code1", "displaY1"); p.getMeta().addTag("system", "code2", "displaY2"); p.addIdentifier().setSystem("urn:system").setValue("2"); - myPatientDao.create(p).getId().toUnqualified(); + myPatientDao.create(p, mySrd).getId().toUnqualified(); }); myCaptureQueriesListener.clear(); @@ -1347,16 +1345,16 @@ public void testSearchAndPageThroughResults_SmallChunksOnSameBundleProvider() { } assertThat(foundIds).hasSize(ids.size()); - ids.sort(new ComparableComparator<>()); - foundIds.sort(new ComparableComparator<>()); + ids.sort(Comparator.naturalOrder()); + foundIds.sort(Comparator.naturalOrder()); assertEquals(ids, foundIds); // This really generates a surprising number of selects and commits. We // could stand to reduce this! myCaptureQueriesListener.logSelectQueries(); assertEquals(56, myCaptureQueriesListener.countSelectQueries()); - assertEquals(71, myCaptureQueriesListener.getCommitCount()); - assertEquals(0, myCaptureQueriesListener.getRollbackCount()); + assertEquals(71, myCaptureQueriesListener.countCommits()); + assertEquals(0, myCaptureQueriesListener.countRollbacks()); } /** @@ -1375,13 +1373,13 @@ public void testSearchAndPageThroughResults_LargeChunksOnIndependentBundleProvid search = myPagingProvider.retrieveResultList(mySrd, search.getUuid()); } - ids.sort(new ComparableComparator<>()); - foundIds.sort(new ComparableComparator<>()); + ids.sort(Comparator.naturalOrder()); + foundIds.sort(Comparator.naturalOrder()); assertEquals(ids, foundIds); assertEquals(22, myCaptureQueriesListener.countSelectQueries()); - assertEquals(21, myCaptureQueriesListener.getCommitCount()); - assertEquals(0, myCaptureQueriesListener.getRollbackCount()); + assertEquals(21, myCaptureQueriesListener.countCommits()); + assertEquals(0, myCaptureQueriesListener.countRollbacks()); } /** @@ -1399,13 +1397,13 @@ public void testSearchAndPageThroughResults_LargeChunksOnSameBundleProvider_Sync nextChunk.forEach(t -> foundIds.add(t.getIdElement().toUnqualifiedVersionless().getValue())); } - ids.sort(new ComparableComparator<>()); - foundIds.sort(new ComparableComparator<>()); + ids.sort(Comparator.naturalOrder()); + foundIds.sort(Comparator.naturalOrder()); assertEquals(ids, foundIds); assertEquals(2, myCaptureQueriesListener.countSelectQueries()); - assertEquals(1, myCaptureQueriesListener.getCommitCount()); - assertEquals(0, myCaptureQueriesListener.getRollbackCount()); + assertEquals(1, myCaptureQueriesListener.countCommits()); + assertEquals(0, myCaptureQueriesListener.countRollbacks()); } @Nonnull @@ -1532,18 +1530,18 @@ public void testSearchUsingForcedIdReference() { Patient patient = new Patient(); patient.setId("P"); patient.setActive(true); - myPatientDao.update(patient); + myPatientDao.update(patient, mySrd); Observation obs = new Observation(); obs.getSubject().setReference("Patient/P"); - myObservationDao.create(obs); + myObservationDao.create(obs, mySrd); SearchParameterMap map = new SearchParameterMap(); map.setLoadSynchronous(true); map.add("subject", new ReferenceParam("Patient/P")); myCaptureQueriesListener.clear(); - assertEquals(1, myObservationDao.search(map).size().intValue()); + assertEquals(1, myObservationDao.search(map, mySrd).sizeOrThrowNpe()); // (not resolve forced ID), Perform search, load result assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertNoPartitionSelectors(); @@ -1556,7 +1554,7 @@ public void testSearchUsingForcedIdReference() { */ myCaptureQueriesListener.clear(); - assertEquals(1, myObservationDao.search(map).size().intValue()); + assertEquals(1, myObservationDao.search(map, mySrd).sizeOrThrowNpe()); myCaptureQueriesListener.logAllQueriesForCurrentThread(); // (not resolve forced ID), Perform search, load result (this time we reuse the cached forced-id resolution) assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); @@ -1576,18 +1574,18 @@ public void testSearchUsingForcedIdReference_DeletedDisabled() { Patient patient = new Patient(); patient.setId("P"); patient.setActive(true); - myPatientDao.update(patient); + myPatientDao.update(patient, mySrd); Observation obs = new Observation(); obs.getSubject().setReference("Patient/P"); - myObservationDao.create(obs); + myObservationDao.create(obs, mySrd); SearchParameterMap map = new SearchParameterMap(); map.setLoadSynchronous(true); map.add("subject", new ReferenceParam("Patient/P")); myCaptureQueriesListener.clear(); - assertEquals(1, myObservationDao.search(map).size().intValue()); + assertEquals(1, myObservationDao.search(map, mySrd).sizeOrThrowNpe()); myCaptureQueriesListener.logAllQueriesForCurrentThread(); // (not Resolve forced ID), Perform search, load result assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); @@ -1600,7 +1598,7 @@ public void testSearchUsingForcedIdReference_DeletedDisabled() { */ myCaptureQueriesListener.clear(); - assertEquals(1, myObservationDao.search(map).size().intValue()); + assertEquals(1, myObservationDao.search(map, mySrd).sizeOrThrowNpe()); myCaptureQueriesListener.logAllQueriesForCurrentThread(); // (NO resolve forced ID), Perform search, load result assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); @@ -1618,16 +1616,16 @@ public void testSearchOnChainedToken() { Patient patient = new Patient(); patient.setId("P"); patient.addIdentifier().setSystem("sys").setValue("val"); - myPatientDao.update(patient); + myPatientDao.update(patient, mySrd); Observation obs = new Observation(); obs.setId("O"); obs.getSubject().setReference("Patient/P"); - myObservationDao.update(obs); + myObservationDao.update(obs, mySrd); SearchParameterMap map = SearchParameterMap.newSynchronous(Observation.SP_SUBJECT, new ReferenceParam("identifier", "sys|val")); myCaptureQueriesListener.clear(); - IBundleProvider outcome = myObservationDao.search(map); + IBundleProvider outcome = myObservationDao.search(map, mySrd); assertThat(toUnqualifiedVersionlessIdValues(outcome)).containsExactlyInAnyOrder("Observation/O"); assertEquals(2, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); @@ -1650,32 +1648,32 @@ public void testSearchOnReverseInclude() { patient.getMeta().addTag("http://system", "value1", "display"); patient.setId("P1"); patient.getNameFirstRep().setFamily("FAM1"); - myPatientDao.update(patient); + myPatientDao.update(patient, mySrd); patient = new Patient(); patient.setId("P2"); patient.getMeta().addTag("http://system", "value1", "display"); patient.getNameFirstRep().setFamily("FAM2"); - myPatientDao.update(patient); + myPatientDao.update(patient, mySrd); for (int i = 0; i < 3; i++) { CareTeam ct = new CareTeam(); ct.setId("CT1-" + i); ct.getMeta().addTag("http://system", "value11", "display"); ct.getSubject().setReference("Patient/P1"); - myCareTeamDao.update(ct); + myCareTeamDao.update(ct, mySrd); ct = new CareTeam(); ct.setId("CT2-" + i); ct.getMeta().addTag("http://system", "value22", "display"); ct.getSubject().setReference("Patient/P2"); - myCareTeamDao.update(ct); + myCareTeamDao.update(ct, mySrd); } SearchParameterMap map = SearchParameterMap.newSynchronous().addRevInclude(CareTeam.INCLUDE_SUBJECT).setSort(new SortSpec(Patient.SP_NAME)); myCaptureQueriesListener.clear(); - IBundleProvider outcome = myPatientDao.search(map); + IBundleProvider outcome = myPatientDao.search(map, mySrd); assertEquals(SimpleBundleProvider.class, outcome.getClass()); assertThat(toUnqualifiedVersionlessIdValues(outcome)).containsExactlyInAnyOrder("Patient/P1", "CareTeam/CT1-0", "CareTeam/CT1-1", "CareTeam/CT1-2", "Patient/P2", "CareTeam/CT2-0", "CareTeam/CT2-1", "CareTeam/CT2-2"); @@ -1924,7 +1922,7 @@ public void testTransactionWithMultipleCreates_PreExistingMatchUrl() { Practitioner pract = new Practitioner(); pract.addIdentifier().setSystem("foo").setValue("bar"); - myPractitionerDao.create(pract); + myPractitionerDao.create(pract, mySrd); runInTransaction(() -> assertEquals(1, myResourceTableDao.count(), () -> myResourceTableDao.findAll().stream().map(t -> t.getIdDt().toUnqualifiedVersionless().getValue()).collect(Collectors.joining(",")))); // First pass @@ -2127,7 +2125,7 @@ public void testTransactionWithMultipleUpdates() { outcome = mySystemDao.transaction(mySrd, input.get()); ourLog.debug("Resp: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); myCaptureQueriesListener.logSelectQueries(); - assertEquals(4, myCaptureQueriesListener.countSelectQueries()); + assertEquals(3, myCaptureQueriesListener.countSelectQueries()); myCaptureQueriesListener.logInsertQueries(); assertEquals(2, myCaptureQueriesListener.countInsertQueries()); myCaptureQueriesListener.logUpdateQueries(); @@ -2210,7 +2208,7 @@ public void testTransactionWithMultipleUpdates_ResourcesHaveTags() { outcome = mySystemDao.transaction(mySrd, input.get()); ourLog.debug("Resp: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); myCaptureQueriesListener.logSelectQueries(); - assertEquals(5, myCaptureQueriesListener.countSelectQueries()); + assertEquals(4, myCaptureQueriesListener.countSelectQueries()); myCaptureQueriesListener.logInsertQueries(); assertEquals(5, myCaptureQueriesListener.countInsertQueries()); myCaptureQueriesListener.logUpdateQueries(); @@ -2334,7 +2332,7 @@ public void testTransactionWithMultipleForcedIdReferences() { Patient pt = new Patient(); pt.setId("ABC"); pt.setActive(true); - myPatientDao.update(pt); + myPatientDao.update(pt, mySrd); Location loc = new Location(); loc.setId("LOC"); @@ -2522,7 +2520,7 @@ public void testTransactionWithMultipleConditionalUpdates() { outcome = mySystemDao.transaction(mySrd, input.get()); ourLog.debug("Resp: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); myCaptureQueriesListener.logSelectQueries(); - assertEquals(7, myCaptureQueriesListener.countSelectQueries()); + assertEquals(6, myCaptureQueriesListener.countSelectQueries()); myCaptureQueriesListener.logInsertQueries(); assertEquals(4, myCaptureQueriesListener.countInsertQueries()); myCaptureQueriesListener.logUpdateQueries(); @@ -2537,7 +2535,7 @@ public void testTransactionWithMultipleConditionalUpdates() { outcome = mySystemDao.transaction(mySrd, input.get()); ourLog.debug("Resp: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); myCaptureQueriesListener.logSelectQueries(); - assertEquals(5, myCaptureQueriesListener.countSelectQueries()); + assertEquals(4, myCaptureQueriesListener.countSelectQueries()); myCaptureQueriesListener.logInsertQueries(); assertEquals(4, myCaptureQueriesListener.countInsertQueries()); myCaptureQueriesListener.logUpdateQueries(); @@ -2579,7 +2577,7 @@ public void testTransactionWithConditionalCreate_MatchUrlCacheEnabled() { assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); runInTransaction(() -> { - List types = myResourceTableDao.findAll().stream().map(t -> t.getResourceType()).collect(Collectors.toList()); + List types = myResourceTableDao.findAll().stream().map(ResourceTable::getResourceType).collect(Collectors.toList()); assertThat(types).containsExactlyInAnyOrder("Patient", "Observation"); }); @@ -2593,7 +2591,7 @@ public void testTransactionWithConditionalCreate_MatchUrlCacheEnabled() { assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); runInTransaction(() -> { - List types = myResourceTableDao.findAll().stream().map(t -> t.getResourceType()).collect(Collectors.toList()); + List types = myResourceTableDao.findAll().stream().map(ResourceTable::getResourceType).collect(Collectors.toList()); assertThat(types).containsExactlyInAnyOrder("Patient", "Observation", "Observation"); }); @@ -2606,7 +2604,7 @@ public void testTransactionWithConditionalCreate_MatchUrlCacheEnabled() { assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); runInTransaction(() -> { - List types = myResourceTableDao.findAll().stream().map(t -> t.getResourceType()).collect(Collectors.toList()); + List types = myResourceTableDao.findAll().stream().map(ResourceTable::getResourceType).collect(Collectors.toList()); assertThat(types).containsExactlyInAnyOrder("Patient", "Observation", "Observation", "Observation"); }); @@ -2644,7 +2642,7 @@ public void testTransactionWithConditionalCreate_MatchUrlCacheNotEnabled() { assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); runInTransaction(() -> { - List types = myResourceTableDao.findAll().stream().map(t -> t.getResourceType()).collect(Collectors.toList()); + List types = myResourceTableDao.findAll().stream().map(ResourceTable::getResourceType).collect(Collectors.toList()); assertThat(types).containsExactlyInAnyOrder("Patient", "Observation"); }); @@ -2663,7 +2661,7 @@ public void testTransactionWithConditionalCreate_MatchUrlCacheNotEnabled() { assertThat(matchUrlQuery).contains("fetch first '2'"); runInTransaction(() -> { - List types = myResourceTableDao.findAll().stream().map(t -> t.getResourceType()).collect(Collectors.toList()); + List types = myResourceTableDao.findAll().stream().map(ResourceTable::getResourceType).collect(Collectors.toList()); assertThat(types).containsExactlyInAnyOrder("Patient", "Observation", "Observation"); }); @@ -2676,7 +2674,7 @@ public void testTransactionWithConditionalCreate_MatchUrlCacheNotEnabled() { assertEquals(0, myCaptureQueriesListener.countDeleteQueries()); runInTransaction(() -> { - List types = myResourceTableDao.findAll().stream().map(t -> t.getResourceType()).collect(Collectors.toList()); + List types = myResourceTableDao.findAll().stream().map(ResourceTable::getResourceType).collect(Collectors.toList()); assertThat(types).containsExactlyInAnyOrder("Patient", "Observation", "Observation", "Observation"); }); @@ -2848,12 +2846,12 @@ public void testTransactionWithMultiplePreExistingReferences_ForcedId() { Patient patient = new Patient(); patient.setId("Patient/A"); patient.setActive(true); - myPatientDao.update(patient); + myPatientDao.update(patient, mySrd); Practitioner practitioner = new Practitioner(); practitioner.setId("Practitioner/B"); practitioner.setActive(true); - myPractitionerDao.update(practitioner); + myPractitionerDao.update(practitioner, mySrd); // Create transaction @@ -2921,11 +2919,11 @@ public void testTransactionWithMultiplePreExistingReferences_Numeric() { Patient patient = new Patient(); patient.setActive(true); - IIdType patientId = myPatientDao.create(patient).getId().toUnqualifiedVersionless(); + IIdType patientId = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); Practitioner practitioner = new Practitioner(); practitioner.setActive(true); - IIdType practitionerId = myPractitionerDao.create(practitioner).getId().toUnqualifiedVersionless(); + IIdType practitionerId = myPractitionerDao.create(practitioner, mySrd).getId().toUnqualifiedVersionless(); // Create transaction Bundle input = new Bundle(); @@ -2994,12 +2992,12 @@ public void testTransactionWithMultiplePreExistingReferences_ForcedId_DeletesDis Patient patient = new Patient(); patient.setId("Patient/A"); patient.setActive(true); - myPatientDao.update(patient); + myPatientDao.update(patient, mySrd); Practitioner practitioner = new Practitioner(); practitioner.setId("Practitioner/B"); practitioner.setActive(true); - myPractitionerDao.update(practitioner); + myPractitionerDao.update(practitioner, mySrd); // Create transaction @@ -3067,11 +3065,11 @@ public void testTransactionWithMultiplePreExistingReferences_Numeric_DeletesDisa Patient patient = new Patient(); patient.setActive(true); - IIdType patientId = myPatientDao.create(patient).getId().toUnqualifiedVersionless(); + IIdType patientId = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); Practitioner practitioner = new Practitioner(); practitioner.setActive(true); - IIdType practitionerId = myPractitionerDao.create(practitioner).getId().toUnqualifiedVersionless(); + IIdType practitionerId = myPractitionerDao.create(practitioner, mySrd).getId().toUnqualifiedVersionless(); // Create transaction Bundle input = new Bundle(); @@ -3139,12 +3137,12 @@ public void testTransactionWithMultiplePreExistingReferences_IfNoneExist() { Patient patient = new Patient(); patient.setId("Patient/A"); patient.setActive(true); - myPatientDao.update(patient); + myPatientDao.update(patient, mySrd); Practitioner practitioner = new Practitioner(); practitioner.setId("Practitioner/B"); practitioner.setActive(true); - myPractitionerDao.update(practitioner); + myPractitionerDao.update(practitioner, mySrd); // Create transaction @@ -3750,7 +3748,7 @@ public void testMassIngestionMode_TransactionWithChanges() { myCaptureQueriesListener.clear(); Bundle outcome = mySystemDao.transaction(new SystemRequestDetails(), supplier.get()); myCaptureQueriesListener.logSelectQueries(); - assertEquals(6, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(5, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); myCaptureQueriesListener.logInsertQueries(); assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); assertEquals(7, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); @@ -3773,7 +3771,7 @@ public void testMassIngestionMode_TransactionWithChanges() { myCaptureQueriesListener.clear(); outcome = mySystemDao.transaction(new SystemRequestDetails(), supplier.get()); - assertEquals(6, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(5, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); myCaptureQueriesListener.logInsertQueries(); assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); assertEquals(6, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); @@ -3834,11 +3832,65 @@ public void testMassIngestionMode_TransactionWithChanges_NonVersionedTags() thro myCaptureQueriesListener.clear(); mySystemDao.transaction(new SystemRequestDetails(), loadResourceFromClasspath(Bundle.class, "r4/transaction-perf-bundle-smallchanges.json")); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertEquals(6, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(5, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); assertEquals(2, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); assertEquals(6, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(1, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); + } + + /** + * See the class javadoc before changing the counts in this test! + */ + @Test + public void testMassIngestionMode_TransactionWithManyUpdates() { + myStorageSettings.setMassIngestionMode(true); + + for (int i = 0; i < 10; i++) { + Organization org = new Organization(); + org.setId("ORG" + i); + org.setName("ORG " + i); + myOrganizationDao.update(org, mySrd); + } + for (int i = 0; i < 5; i++) { + Patient patient = new Patient(); + patient.setId("PT" + i); + patient.setActive(true); + patient.setManagingOrganization(new Reference("Organization/ORG" + i)); + myPatientDao.update(patient, mySrd); + } + + Supplier supplier = () -> { + BundleBuilder bb = new BundleBuilder(myFhirContext); + + for (int i = 0; i < 10; i++) { + Patient patient = new Patient(); + patient.setId("PT" + i); + // Flip this value + patient.setActive(false); + patient.addIdentifier().setSystem("http://foo").setValue("bar"); + patient.setManagingOrganization(new Reference("Organization/ORG" + i)); + bb.addTransactionUpdateEntry(patient); + } + + return (Bundle) bb.getBundle(); + }; + + // Test + + myCaptureQueriesListener.clear(); + myMemoryCacheService.invalidateAllCaches(); + mySystemDao.transaction(new SystemRequestDetails(), supplier.get()); + myCaptureQueriesListener.logSelectQueries(); + myCaptureQueriesListener.logInsertQueries(); + assertEquals(3, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(40, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); + assertEquals(10, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); + assertEquals(0, myCaptureQueriesListener.countInsertQueriesRepeated()); + + + } /** @@ -3860,7 +3912,7 @@ public void testDeleteResource_WithOutgoingReference() { assertEquals(1, myCaptureQueriesListener.countUpdateQueriesForCurrentThread()); assertEquals(3, myCaptureQueriesListener.countDeleteQueriesForCurrentThread()); runInTransaction(() -> { - ResourceTable version = myResourceTableDao.findById(patientId.getIdPartAsLong()).orElseThrow(); + ResourceTable version = myResourceTableDao.findById(JpaPid.fromId(patientId.getIdPartAsLong())).orElseThrow(); assertFalse(version.isParamsTokenPopulated()); assertFalse(version.isHasLinks()); assertEquals(0, myResourceIndexedSearchParamTokenDao.count()); @@ -3882,7 +3934,7 @@ public void testDeleteResource_WithMassIngestionMode_enabled() { IIdType idDt = myObservationDao.create(observation, mySrd).getEntity().getIdDt(); runInTransaction(() -> { assertEquals(4, myResourceIndexedSearchParamTokenDao.count()); - ResourceTable version = myResourceTableDao.findById(idDt.getIdPartAsLong()).orElseThrow(); + ResourceTable version = myResourceTableDao.findById(JpaPid.fromId(idDt.getIdPartAsLong())).orElseThrow(); assertTrue(version.isParamsTokenPopulated()); }); @@ -3894,7 +3946,7 @@ public void testDeleteResource_WithMassIngestionMode_enabled() { assertQueryCount(3, 1, 1, 2); runInTransaction(() -> { assertEquals(0, myResourceIndexedSearchParamTokenDao.count()); - ResourceTable version = myResourceTableDao.findById(idDt.getIdPartAsLong()).orElseThrow(); + ResourceTable version = myResourceTableDao.findById(JpaPid.fromId(idDt.getIdPartAsLong())).orElseThrow(); assertFalse(version.isParamsTokenPopulated()); }); } @@ -3950,12 +4002,10 @@ public void testValidateResource(boolean theStoredInRepository) { String encoded; IIdType id = null; - int initialAdditionalSelects = 0; if (theStoredInRepository) { id = myPatientDao.create(resource, mySrd).getId(); resource = null; encoded = null; - initialAdditionalSelects = 1; } else { resource.setId("A"); encoded = myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(resource); @@ -3967,8 +4017,8 @@ public void testValidateResource(boolean theStoredInRepository) { assertThat(((OperationOutcome)outcome.getOperationOutcome()).getIssueFirstRep().getDiagnostics()).contains("No issues detected"); myCaptureQueriesListener.logSelectQueries(); if (theStoredInRepository) { - assertEquals(7, myCaptureQueriesListener.countGetConnections()); - assertEquals(8, myCaptureQueriesListener.countSelectQueries()); + assertEquals(5, myCaptureQueriesListener.countGetConnections()); + assertEquals(6, myCaptureQueriesListener.countSelectQueries()); } else { assertEquals(6, myCaptureQueriesListener.countGetConnections()); assertEquals(6, myCaptureQueriesListener.countSelectQueries()); @@ -4149,7 +4199,7 @@ private IIdType createAPatient() { Patient p = new Patient(); p.getMeta().addTag("http://system", "foo", "display"); p.addIdentifier().setSystem("urn:system").setValue("2"); - return myPatientDao.create(p).getId().toUnqualified(); + return myPatientDao.create(p, mySrd).getId().toUnqualified(); }); } diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java index 7e2ab8dc9989..64ba2efa2d65 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/dao/r4/PartitioningSqlR4Test.java @@ -116,7 +116,7 @@ public class PartitioningSqlR4Test extends BasePartitioningR4Test { @BeforeEach public void disableAdvanceIndexing() { - myStorageSettings.setAdvancedHSearchIndexing(false); + myStorageSettings.setHibernateSearchIndexSearchParams(false); // ugh - somewhere the hibernate round trip is mangling LocalDate to h2 date column unless the tz=GMT TimeZone.setDefault(TimeZone.getTimeZone("GMT")); ourLog.info("Running with Timezone {}", TimeZone.getDefault().getID()); @@ -3053,7 +3053,7 @@ public void testTransaction_MultipleConditionalUpdates() { outcome = mySystemDao.transaction(mySrd, input.get()); ourLog.debug("Resp: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertEquals(7, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(6, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); myCaptureQueriesListener.logInsertQueriesForCurrentThread(); assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); @@ -3069,7 +3069,7 @@ public void testTransaction_MultipleConditionalUpdates() { outcome = mySystemDao.transaction(mySrd, input.get()); ourLog.debug("Resp: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(outcome)); myCaptureQueriesListener.logSelectQueriesForCurrentThread(); - assertEquals(5, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); + assertEquals(4, myCaptureQueriesListener.countSelectQueriesForCurrentThread()); myCaptureQueriesListener.logInsertQueriesForCurrentThread(); assertEquals(4, myCaptureQueriesListener.countInsertQueriesForCurrentThread()); myCaptureQueriesListener.logUpdateQueriesForCurrentThread(); diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/validation/RepositoryValidatingInterceptorR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/validation/RepositoryValidatingInterceptorR4Test.java index 88500159e247..4db1f9e4b366 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/validation/RepositoryValidatingInterceptorR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/interceptor/validation/RepositoryValidatingInterceptorR4Test.java @@ -1,7 +1,5 @@ package ca.uhn.fhir.jpa.interceptor.validation; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import ca.uhn.fhir.i18n.Msg; import ca.uhn.fhir.jpa.test.BaseJpaR4Test; import ca.uhn.fhir.rest.api.PatchTypeEnum; @@ -30,6 +28,8 @@ import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; +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 RepositoryValidatingInterceptorR4Test extends BaseJpaR4Test { @@ -238,7 +238,6 @@ public void testDisallowProfile_UpdateBlocked() { patient = myPatientDao.read(id); assertThat(patient.getMeta().getProfile().stream().map(t -> t.getValue()).collect(Collectors.toList())).containsExactlyInAnyOrder("http://foo/Profile1"); - } @Test @@ -386,13 +385,11 @@ public void testRequireValidation_Blocked() { OperationOutcome oo = (OperationOutcome) e.getOperationOutcome(); assertThat(oo.getIssue().get(0).getDiagnostics()).contains("Observation.status: minimum required = 1, but only found 0"); } - } @Test public void testMultipleTypedRules() { - List rules = newRuleBuilder() .forResourcesOfType("Observation") .requireAtLeastProfile("http://hl7.org/fhir/StructureDefinition/Observation") diff --git a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerR4Test.java b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerR4Test.java index 94ccdd3ac59a..c101867dddbd 100644 --- a/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerR4Test.java +++ b/hapi-fhir-jpaserver-test-r4/src/test/java/ca/uhn/fhir/jpa/provider/r4/ServerR4Test.java @@ -1,36 +1,54 @@ package ca.uhn.fhir.jpa.provider.r4; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; +import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingInterceptor; +import ca.uhn.fhir.jpa.interceptor.validation.RepositoryValidatingRuleBuilder; import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.parser.LenientErrorHandler; +import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.EncodingEnum; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; import ca.uhn.fhir.util.ExtensionConstants; import org.apache.commons.io.IOUtils; import org.apache.http.Header; +import org.apache.http.HttpStatus; 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.HttpPut; +import org.apache.http.entity.StringEntity; import org.hl7.fhir.r4.model.CapabilityStatement; import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceComponent; import org.hl7.fhir.r4.model.CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent; import org.hl7.fhir.r4.model.Extension; +import org.hl7.fhir.r4.model.OperationOutcome; import org.hl7.fhir.r4.model.Patient; +import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.testcontainers.shaded.com.google.common.collect.HashMultimap; +import org.testcontainers.shaded.com.google.common.collect.Multimap; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; +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; @@ -61,6 +79,367 @@ public void testCapabilityStatementValidates() throws IOException { } } + private static class DemoValidationInterceptor extends RepositoryValidatingInterceptor { + + @Autowired + private FhirContext myFhirContext; + + @Autowired + private ApplicationContext myApplicationContext; + + public DemoValidationInterceptor(FhirContext theFhirContext, ApplicationContext theContext) { + super(); + myFhirContext = theFhirContext; + myApplicationContext = theContext; + } + + public void start() { + setFhirContext(myFhirContext); + + // Ask the application context for a new Rule Builder + RepositoryValidatingRuleBuilder ruleBuilder = + myApplicationContext.getBean(RepositoryValidatingRuleBuilder.class); + + // we only want the basic validation profiles + ruleBuilder + .forResourcesOfType("Patient") + .requireValidationToDeclaredProfiles(); + + // Create the ruleset and pass it to the interceptor + setRules(ruleBuilder.build()); + } + } + + static List validationTestParameters() { + @Language("JSON") + String patientStr; + List arguments = new ArrayList<>(); + + // 1 The full resource from bug report + { + patientStr = """ + { + "resourceType" : "Patient", + "id" : "P12312", + "meta" : { + "profile" : ["http://hl7.org/fhir/StructureDefinition/Patient"] + }, + "extension" : [ { + "url" : "http://hl7.org/fhir/StructureDefinition/us-core-ethnicity", + "extension" : [ { + "url" : "ombCategory", + "valueCoding" : { + "code" : "2186-5", + "display" : "Not Hispanic or Latino", + "system" : "urn:oid:2.16.840.1.113883.6.238" + } + }, { + "url" : "text", + "valueString" : "Non-Hisp" + } ] + }, { + "url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "extension" : [ { + "url" : "ombCategory", + "valueCoding" : { + "code" : "2054-5", + "display" : "Black or African American", + "system" : "urn:oid:2.16.840.1.113883.6.238" + } + }, { + "url" : "text", + "valueString" : "Black" + } ] + }, { + "url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", + "valueCode" : "M" + } ], + "communication" : [ { + "language" : { + "coding" : [ { + "code" : "en", + "display" : "English", + "system" : "urn:ietf:bcp:47" + }, { + "code" : "ENG", + "display" : "English", + "system" : "http://fkcfhir.org/fhir/CodeSystem/fmc-language-cs" + } ], + "text" : "EN" + }, + "preferred" : true + } ], + "telecom" : [ { + "system" : "phone", + "value" : "393-342-2312" + } ], + "identifier" : [ { + "system" : "http://hl7.org/fhir/sid/us-ssn", + "type" : { + "coding" : [ { + "system" : "http://terminology.hl7.org/CodeSystem/v2-0203", + "code" : "SS", + "display" : "Social Security Number" + } ], + "text" : "Social Security Number" + }, + "value" : "12133121" + }, { + "system" : "urn:oid:2.16.840.1.113883.3.7418.2.1", + "type" : { + "coding" : [ { + "system" : "http://terminology.hl7.org/CodeSystem/v2-0203", + "code" : "MR", + "display" : "Medical record number" + } ], + "text" : "Medical record number" + }, + "value" : "12312" + } ], + "name" : [ { + "use" : "official", + "family" : "WEIHE", + "given" : [ "FLOREZ,A" ], + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + } ], + "gender" : "male", + "birthDate" : "1955-09-19", + "active" : true, + "address" : [ { + "type" : "postal", + "line" : [ "1553 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + }, { + "type" : "physical", + "use" : "home", + "line" : [ "1553 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + } ], + "maritalStatus" : [ { + "coding" : [ { + "code" : "S", + "display" : "Never Married", + "system" : "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus" + } ], + "text" : "S" + } ], + "contact" : [ + { + "relationship" : [ { + "coding" : [ { + "code" : "PRN", + "display" : "parent", + "system" : "http://terminology.hl7.org/CodeSystem/v3-RoleCode" + } ], + "text" : "Parnt" + } ], + "name" : [ { + "use" : "official", + "family" : "PRESTIDGE", + "given" : [ "HEINEMAN" ] + } ], + "address" : [ { + "type" : "postal", + "line" : [ "1553 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + }, { + "type" : "physical", + "use" : "home", + "line" : [ "1553 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + } ], + "extension" : [ { + "url" : "http://fkcfhir.org/fhir/StructureDefinition/fmc-patient-contact-type", + "valueCodeableConcept" : { + "coding" : [ { + "system" : "http://fkcfhir.org/fhir/CodeSystem/fmc-patient-contact-type-cs", + "code" : "PRIMARY", + "display" : "Primary Contact" + } ], + "text" : "Emergency" + } + } ] + }, + { + "relationship" : [ { + "coding" : [ { + "code" : "E", + "display" : "Employer", + "system" : "http://terminology.hl7.org/CodeSystem/v2-0131" + } ], + "text" : "EMP" + } ], + "address" : [ { + "type" : "postal", + "line" : [ "1553 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + }, { + "type" : "physical", + "use" : "home", + "line" : [ "1553 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + } ], + "extension" : [ { + "url" : "http://fkcfhir.org/fhir/StructureDefinition/fmc-patient-contact-type", + "valueCodeableConcept" : { + "coding" : [ { + "system" : "http://fkcfhir.org/fhir/CodeSystem/fmc-patient-contact-type-cs", + "code" : "EMPLOYER", + "display" : "Employer" + } ] + } + }, { + "url" : "http://fkcfhir.org/fhir/StructureDefinition/fmc-patient-contact-primary-emp-ind", + "valueBoolean" : false + }, { + "url" : "http://fkcfhir.org/fhir/StructureDefinition/fmc-patient-contact-emp-status", + "valueString" : "jobStatus" + }] + } ] + } + """; + arguments.add( + Arguments.of(patientStr, "P12312") + ); + } + + // 2 A sample resource + { + /* + * This is an invalid patient resource. + * Patient.contact.name has a cardinality of + * 0..1 (so an array should fail). + * + * Our parser can easily handle this (and doesn't care about + * the cardinality), but our endpoints should. + */ + patientStr = """ + { + "resourceType": "Patient", + "id": "P1212", + "contact": [{ + "name": [{ + "use": "official", + "family": "Simpson", + "given": ["Homer" ] + }] + }], + "text": { + "status": "additional", + "div": "
a div element
" + } + } + """; + arguments.add( + Arguments.of(patientStr, "P1212") + ); + } + + return arguments; + } + + @ParameterizedTest + @MethodSource("validationTestParameters") + public void validationTest_invalidResourceWithLenientParsing_createAndValidateShouldParse(String thePatientStr, String theId) throws IOException { + /* + * We also require a lenient error handler (the default case). + * BaseJpaR4Test.before resets this to a StrictErrorHandler, + * which breaks this test, but also means we don't have to reset it. + */ + myFhirContext.setParserErrorHandler(new LenientErrorHandler()); + + IParser parser = myFhirContext.newJsonParser(); + + DemoValidationInterceptor validatingInterceptor = new DemoValidationInterceptor(myFhirContext, myApplicationContext); + validatingInterceptor.start(); + + myServer.getInterceptorService().registerInterceptor(validatingInterceptor); + try { + StringEntity entity = new StringEntity(thePatientStr, StandardCharsets.UTF_8); + + OperationOutcome validationOutcome; + OperationOutcome createOutcome; + + HttpPost post = new HttpPost(myServerBase + "/Patient/$validate"); + post.addHeader(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON_NEW); + post.setEntity(entity); + try (CloseableHttpResponse resp = ourHttpClient.execute(post)) { + assertEquals(HttpStatus.SC_OK, resp.getStatusLine().getStatusCode()); + + validationOutcome = getOutcome(resp, parser); + } + + HttpPut put = new HttpPut(myServerBase + "/Patient/" + theId); + put.addHeader(Constants.HEADER_CONTENT_TYPE, Constants.CT_FHIR_JSON_NEW); + put.setEntity(entity); + try (CloseableHttpResponse resp = ourHttpClient.execute(put)) { + assertEquals(HttpStatus.SC_PRECONDITION_FAILED, resp.getStatusLine().getStatusCode()); + + createOutcome = getOutcome(resp, parser); + } + + assertNotNull(validationOutcome); + assertNotNull(createOutcome); + + assertEquals(validationOutcome.getIssue().size(), createOutcome.getIssue().size()); + + Multimap severityToIssue = HashMultimap.create(); + validationOutcome.getIssue() + .forEach(issue -> { + severityToIssue.put(issue.getSeverity(), issue.getDiagnostics()); + }); + createOutcome.getIssue() + .forEach(issue -> { + assertTrue(severityToIssue.containsEntry(issue.getSeverity(), issue.getDiagnostics())); + }); + } finally { + myServer.getInterceptorService().unregisterInterceptor(validatingInterceptor); + } + } + + private OperationOutcome getOutcome(CloseableHttpResponse theResponse, IParser theParser) throws IOException { + String content = IOUtils.toString(theResponse.getEntity().getContent(), StandardCharsets.UTF_8); + + return theParser.parseResource(OperationOutcome.class, content); + } /** * See #519 diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdR5Test.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdR5Test.java index c7b08aa39c42..8a7e2d04d9c6 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdR5Test.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/subscription/websocket/WebsocketWithSubscriptionIdR5Test.java @@ -112,13 +112,7 @@ public void testSubscriptionMessagePayloadContentIsEmpty() { // Then List messages = myWebsocketClientExtension.getMessages(); - await().until(() -> !messages.isEmpty()); - - // Log it - ourLog.info("Messages: {}", messages); - - // Verify a ping message shall be returned - Assertions.assertTrue(messages.contains("ping " + subscriptionId)); + await().until(() -> messages, t -> t.contains("ping " + subscriptionId)); } @Test diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java index aef2a96a9ad0..b702eb86d36b 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/BaseJpaTest.java @@ -139,6 +139,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationContext; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.TestPropertySource; @@ -303,6 +304,9 @@ public abstract class BaseJpaTest extends BaseTest { @Autowired private IResourceHistoryTagDao myResourceHistoryTagDao; + @Autowired + protected ApplicationContext myApplicationContext; + @SuppressWarnings("BusyWait") public static void waitForSize(int theTarget, List theList) { StopWatch sw = new StopWatch(); diff --git a/hapi-fhir-sql-migrate/pom.xml b/hapi-fhir-sql-migrate/pom.xml index 78047b801df3..f4ce43d4bbfb 100644 --- a/hapi-fhir-sql-migrate/pom.xml +++ b/hapi-fhir-sql-migrate/pom.xml @@ -54,13 +54,15 @@ org.apache.derby derby test - 10.17.1.0 + 10.16.1.1 + org.apache.derby derbytools test - 10.17.1.0 + 10.16.1.1 + org.postgresql diff --git a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/HapiMigrationStorageSvc.java b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/HapiMigrationStorageSvc.java index a5195a69fda4..1cccc16e327d 100644 --- a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/HapiMigrationStorageSvc.java +++ b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/HapiMigrationStorageSvc.java @@ -80,6 +80,9 @@ public void saveTask(BaseTask theBaseTask, Integer theMillis, boolean theSuccess HapiMigrationEntity entity = HapiMigrationEntity.fromBaseTask(theBaseTask); entity.setExecutionTime(theMillis); entity.setSuccess(theSuccess); + if (theBaseTask.getExecutionResult() != null) { + entity.setResult(theBaseTask.getExecutionResult().name()); + } myHapiMigrationDao.save(entity); } diff --git a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/HapiMigrator.java b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/HapiMigrator.java index 91dd26fe2286..82758514dec7 100644 --- a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/HapiMigrator.java +++ b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/HapiMigrator.java @@ -209,6 +209,7 @@ private void executeTask(BaseTask theTask, MigrationResult theMigrationResult) { theTask.execute(); recordTaskAsCompletedIfNotDryRun(theTask, sw.getMillis(), true); theMigrationResult.changes += theTask.getChangesCount(); + theMigrationResult.executionResult = theTask.getExecutionResult(); theMigrationResult.executedStatements.addAll(theTask.getExecutedStatements()); theMigrationResult.succeededTasks.add(theTask); } catch (SQLException | HapiMigrationException e) { diff --git a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/MigrationResult.java b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/MigrationResult.java index ef0494736b50..6723d47a67b4 100644 --- a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/MigrationResult.java +++ b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/MigrationResult.java @@ -20,6 +20,7 @@ package ca.uhn.fhir.jpa.migrate; import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask; +import ca.uhn.fhir.jpa.migrate.taskdef.MigrationTaskExecutionResultEnum; import java.util.ArrayList; import java.util.List; @@ -29,6 +30,7 @@ public class MigrationResult { public final List executedStatements = new ArrayList<>(); public final List succeededTasks = new ArrayList<>(); public final List failedTasks = new ArrayList<>(); + public MigrationTaskExecutionResultEnum executionResult; public String summary() { return String.format( diff --git a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/dao/HapiMigrationDao.java b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/dao/HapiMigrationDao.java index 5aae666c4e47..709538b1a049 100644 --- a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/dao/HapiMigrationDao.java +++ b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/dao/HapiMigrationDao.java @@ -106,6 +106,11 @@ private Integer getHighestKey() { public boolean createMigrationTableIfRequired() { if (migrationTableExists()) { + if (!columnExists("result")) { + String addResultColumnStatement = myMigrationQueryBuilder.addResultColumnStatement(); + ourLog.info(addResultColumnStatement); + myJdbcTemplate.execute(addResultColumnStatement); + } return false; } ourLog.info("Creating table {}", myMigrationTablename); @@ -144,6 +149,29 @@ private boolean migrationTableExists() { } } + private boolean columnExists(String theColumnName) { + try (Connection connection = myDataSource.getConnection()) { + ResultSet columnsUpper = connection + .getMetaData() + .getColumns( + connection.getCatalog(), + connection.getSchema(), + myMigrationTablename, + theColumnName.toUpperCase()); + ResultSet columnsLower = connection + .getMetaData() + .getColumns( + connection.getCatalog(), + connection.getSchema(), + myMigrationTablename, + theColumnName.toLowerCase()); + + return columnsUpper.next() || columnsLower.next(); // If there's a row, the column exists + } catch (SQLException e) { + throw new InternalErrorException(Msg.code(2615) + "Error checking column existence: " + e.getMessage(), e); + } + } + public List findAll() { String allQuery = myMigrationQueryBuilder.findAllQuery(); ourLog.debug("Executing query: [{}]", allQuery); diff --git a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/dao/MigrationQueryBuilder.java b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/dao/MigrationQueryBuilder.java index 8c6ff2b7b066..43e305cc47a8 100644 --- a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/dao/MigrationQueryBuilder.java +++ b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/dao/MigrationQueryBuilder.java @@ -23,6 +23,7 @@ import ca.uhn.fhir.jpa.migrate.entity.HapiMigrationEntity; import ca.uhn.fhir.jpa.migrate.taskdef.ColumnTypeEnum; import ca.uhn.fhir.jpa.migrate.taskdef.ColumnTypeToDriverTypeToSqlType; +import com.healthmarketscience.sqlbuilder.AlterTableQuery; import com.healthmarketscience.sqlbuilder.BinaryCondition; import com.healthmarketscience.sqlbuilder.CreateIndexQuery; import com.healthmarketscience.sqlbuilder.CreateTableQuery; @@ -55,6 +56,7 @@ public class MigrationQueryBuilder { private final DbColumn myInstalledOnCol; private final DbColumn myExecutionTimeCol; private final DbColumn mySuccessCol; + private final DbColumn myResultCol; private final String myDeleteAll; private final String myHighestKeyQuery; private final DriverTypeEnum myDriverType; @@ -102,6 +104,8 @@ public MigrationQueryBuilder(DriverTypeEnum theDriverType, String theMigrationTa mySuccessCol = myTable.addColumn("\"success\"", myBooleanType, null); mySuccessCol.notNull(); + myResultCol = myTable.addColumn("\"result\"", Types.VARCHAR, HapiMigrationEntity.RESULT_MAX_SIZE); + myDeleteAll = new DeleteQuery(myTable).toString(); myHighestKeyQuery = buildHighestKeyQuery(); } @@ -133,7 +137,8 @@ public String insertPreparedStatement() { myInstalledByCol, myInstalledOnCol, myExecutionTimeCol, - mySuccessCol) + mySuccessCol, + myResultCol) .validate() .toString(); } @@ -142,6 +147,10 @@ public String createTableStatement() { return new CreateTableQuery(myTable, true).validate().toString(); } + public String addResultColumnStatement() { + return new AlterTableQuery(myTable).setAddColumn(myResultCol).validate().toString(); + } + public String createIndexStatement() { return new CreateIndexQuery(myTable, myMigrationTablename.toUpperCase() + "_PK_INDEX") .setIndexType(CreateIndexQuery.IndexType.UNIQUE) diff --git a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/entity/HapiMigrationEntity.java b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/entity/HapiMigrationEntity.java index f58f9ac74501..3dd7801cfc73 100644 --- a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/entity/HapiMigrationEntity.java +++ b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/entity/HapiMigrationEntity.java @@ -41,6 +41,7 @@ public class HapiMigrationEntity { public static final int TYPE_MAX_SIZE = 20; public static final int SCRIPT_MAX_SIZE = 1000; public static final int INSTALLED_BY_MAX_SIZE = 100; + public static final int RESULT_MAX_SIZE = 100; public static final int CREATE_TABLE_PID = -1; public static final String INITIAL_RECORD_DESCRIPTION = "<< HAPI FHIR Schema History table created >>"; public static final String INITIAL_RECORD_SCRIPT = "HAPI FHIR"; @@ -80,6 +81,9 @@ public class HapiMigrationEntity { @Column(name = "SUCCESS") private Boolean mySuccess; + @Column(name = "RESULT", length = RESULT_MAX_SIZE) + private String myResult; + public static HapiMigrationEntity tableCreatedRecord() { HapiMigrationEntity retVal = new HapiMigrationEntity(); retVal.setPid(CREATE_TABLE_PID); @@ -195,6 +199,7 @@ public static RowMapper rowMapper() { entity.setInstalledOn(rs.getDate(8)); entity.setExecutionTime(rs.getInt(9)); entity.setSuccess(rs.getBoolean(10)); + entity.setResult(rs.getString(11)); return entity; }; } @@ -219,6 +224,15 @@ public PreparedStatementSetter asPreparedStatementSetter() { : null); ps.setInt(9, getExecutionTime()); ps.setBoolean(10, getSuccess()); + ps.setString(11, getResult()); }; } + + public String getResult() { + return myResult; + } + + public void setResult(String theResult) { + myResult = theResult; + } } diff --git a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java index 0cb0bcdc0ad7..aa8bf3f98913 100644 --- a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java +++ b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/BaseTask.java @@ -74,6 +74,7 @@ public abstract class BaseTask { private DriverTypeEnum myDriverType; private String myDescription; private Integer myChangesCount = 0; + private MigrationTaskExecutionResultEnum myExecutionResult; private boolean myDryRun; private boolean myTransactional = true; private Set myOnlyAppliesToPlatforms = new HashSet<>(); @@ -210,6 +211,7 @@ private int doExecuteSql(@Language("SQL") String theSql, Object... theArguments) } else { int changesCount = jdbcTemplate.update(theSql, theArguments); logInfo(ourLog, "SQL \"{}\" returned {}", theSql, changesCount); + myExecutionResult = MigrationTaskExecutionResultEnum.APPLIED; return changesCount; } } catch (DataAccessException e) { @@ -218,6 +220,7 @@ private int doExecuteSql(@Language("SQL") String theSql, Object... theArguments) "Task {} did not exit successfully on doExecuteSql(), but task is allowed to fail", getMigrationVersion()); ourLog.debug("Error was: {}", e.getMessage(), e); + myExecutionResult = MigrationTaskExecutionResultEnum.NOT_APPLIED_ALLOWED_FAILURE; return 0; } else { throw new HapiMigrationException( @@ -262,11 +265,13 @@ public JdbcTemplate newJdbcTemplate() { public void execute() throws SQLException { if (myFlags.contains(TaskFlagEnum.DO_NOTHING)) { ourLog.info("Skipping stubbed task: {}", getDescription()); + myExecutionResult = MigrationTaskExecutionResultEnum.NOT_APPLIED_SKIPPED; return; } if (!myOnlyAppliesToPlatforms.isEmpty()) { if (!myOnlyAppliesToPlatforms.contains(getDriverType())) { ourLog.info("Skipping task {} as it does not apply to {}", getDescription(), getDriverType()); + myExecutionResult = MigrationTaskExecutionResultEnum.NOT_APPLIED_NOT_FOR_THIS_DATABASE; return; } } @@ -277,6 +282,7 @@ public void execute() throws SQLException { ourLog.info( "Skipping task since one of the preconditions was not met: {}", precondition.getPreconditionReason()); + myExecutionResult = MigrationTaskExecutionResultEnum.NOT_APPLIED_PRECONDITION_NOT_MET; return; } } @@ -356,6 +362,10 @@ public boolean hasFlag(TaskFlagEnum theFlag) { return myFlags.contains(theFlag); } + public MigrationTaskExecutionResultEnum getExecutionResult() { + return myExecutionResult; + } + public static class ExecutedStatement { private final String mySql; private final List myArguments; diff --git a/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/MigrationTaskExecutionResultEnum.java b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/MigrationTaskExecutionResultEnum.java new file mode 100644 index 000000000000..6aaf8cf05a4b --- /dev/null +++ b/hapi-fhir-sql-migrate/src/main/java/ca/uhn/fhir/jpa/migrate/taskdef/MigrationTaskExecutionResultEnum.java @@ -0,0 +1,28 @@ +package ca.uhn.fhir.jpa.migrate.taskdef; + +public enum MigrationTaskExecutionResultEnum { + /** + * Was either skipped via the `skip-versions` flag or the migration task was stubbed + */ + NOT_APPLIED_SKIPPED, + + /** + * This migration task does not apply to this database + */ + NOT_APPLIED_NOT_FOR_THIS_DATABASE, + + /** + * This migration task had precondition criteria (expressed as SQL) that was not met + */ + NOT_APPLIED_PRECONDITION_NOT_MET, + + /** + * The migration failed, but the task has the FAILURE_ALLOWED flag set. + */ + NOT_APPLIED_ALLOWED_FAILURE, + + /** + * The migration was applied + */ + APPLIED, +} diff --git a/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/BaseMigrationTest.java b/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/BaseMigrationTest.java index 84af0886c3de..ea4cd27e2e3e 100644 --- a/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/BaseMigrationTest.java +++ b/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/BaseMigrationTest.java @@ -17,7 +17,7 @@ public static void beforeAll() { ourHapiMigrationStorageSvc = new HapiMigrationStorageSvc(ourHapiMigrationDao); } - static BasicDataSource getDataSource() { + public static BasicDataSource getDataSource() { BasicDataSource retVal = new BasicDataSource(); retVal.setDriver(new org.h2.Driver()); retVal.setUrl("jdbc:h2:mem:test_migration"); diff --git a/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/SchemaMigratorTest.java b/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/SchemaMigratorTest.java index 1af056b49101..34bfab776172 100644 --- a/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/SchemaMigratorTest.java +++ b/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/SchemaMigratorTest.java @@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.migrate.taskdef.AddTableRawSqlTask; import ca.uhn.fhir.jpa.migrate.taskdef.BaseTask; import ca.uhn.fhir.jpa.migrate.taskdef.BaseTest; +import ca.uhn.fhir.jpa.migrate.taskdef.MigrationTaskExecutionResultEnum; import ca.uhn.fhir.jpa.migrate.tasks.api.TaskFlagEnum; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; @@ -25,6 +26,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -164,6 +167,14 @@ public void testSkipSchemaVersion(Supplier theTestDatabaseD DriverTypeEnum.ConnectionProperties connectionProperties = super.getDriverType().newConnectionProperties(getDataSource().getUrl(), getDataSource().getUsername(), getDataSource().getPassword()); Set tableNames = JdbcUtils.getTableNames(connectionProperties); assertThat(tableNames).containsExactlyInAnyOrder("SOMETABLE_A", "SOMETABLE_C"); + + List entities = myHapiMigrationDao.findAll(); + + assertThat(entities).hasSize(4); + assertThat(entities.get(0).getResult()).isEqualTo(MigrationTaskExecutionResultEnum.APPLIED.name()); + assertThat(entities.get(1).getResult()).isEqualTo(MigrationTaskExecutionResultEnum.NOT_APPLIED_SKIPPED.name()); + assertThat(entities.get(2).getResult()).isEqualTo(MigrationTaskExecutionResultEnum.APPLIED.name()); + assertThat(entities.get(3).getResult()).isEqualTo(MigrationTaskExecutionResultEnum.NOT_APPLIED_SKIPPED.name()); } @Nonnull diff --git a/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/dao/AlterMigrationTableIT.java b/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/dao/AlterMigrationTableIT.java new file mode 100644 index 000000000000..d78aa0c43eda --- /dev/null +++ b/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/dao/AlterMigrationTableIT.java @@ -0,0 +1,75 @@ +package ca.uhn.fhir.jpa.migrate.dao; + +import ca.uhn.fhir.jpa.migrate.BaseMigrationTest; +import ca.uhn.fhir.jpa.migrate.DriverTypeEnum; +import ca.uhn.fhir.jpa.migrate.HapiMigrationStorageSvc; +import org.apache.commons.dbcp2.BasicDataSource; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AlterMigrationTableIT { + private static final Logger ourLog = LoggerFactory.getLogger(AlterMigrationTableIT.class); + + static final String TABLE_NAME = "TEST_MIGRATION_TABLE"; + protected static HapiMigrationDao ourHapiMigrationDao; + static JdbcTemplate ourJdbcTemplate; + protected static HapiMigrationStorageSvc ourHapiMigrationStorageSvc; + + @BeforeAll + public static void beforeAll() { + BasicDataSource dataSource = BaseMigrationTest.getDataSource(); + ourHapiMigrationDao = new HapiMigrationDao(dataSource, DriverTypeEnum.H2_EMBEDDED, TABLE_NAME); + ourJdbcTemplate = new JdbcTemplate(dataSource); + createOldTable(); + } + + private static void createOldTable() { + String oldSchema = """ + CREATE TABLE "TEST_MIGRATION_TABLE" ( + "installed_rank" INTEGER NOT NULL, + "version" VARCHAR(50),"description" VARCHAR(200) NOT NULL, + "type" VARCHAR(20) NOT NULL,"script" VARCHAR(1000) NOT NULL, + "checksum" INTEGER, + "installed_by" VARCHAR(100) NOT NULL, + "installed_on" DATE NOT NULL, + "execution_time" INTEGER NOT NULL, + "success" boolean NOT NULL) + """; + ourJdbcTemplate.execute(oldSchema); + } + + @Test + void testNewColumnAdded() { + assertFalse(doesColumnExist("TEST_MIGRATION_TABLE", "RESULT")); + ourHapiMigrationDao.createMigrationTableIfRequired(); + assertTrue(doesColumnExist("TEST_MIGRATION_TABLE", "RESULT")); + } + + private boolean doesColumnExist(String theTableName, String theColumnName) { + String sql = """ + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_NAME = ? + """; + + List columns = ourJdbcTemplate.query( + sql, + new Object[] { theTableName.toUpperCase() }, + (rs, rowNum) -> rs.getString("COLUMN_NAME") + ); + + ourLog.info("Columns in table '{}': {}", theTableName, columns); + + return columns.stream() + .map(String::toUpperCase) + .anyMatch(columnName -> columnName.equals(theColumnName.toUpperCase())); + } +} diff --git a/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ModifyColumnTest.java b/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ModifyColumnTest.java index c6cdce4098e0..dcd377b402f7 100644 --- a/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ModifyColumnTest.java +++ b/hapi-fhir-sql-migrate/src/test/java/ca/uhn/fhir/jpa/migrate/taskdef/ModifyColumnTest.java @@ -3,12 +3,14 @@ import ca.uhn.fhir.jpa.migrate.DriverTypeEnum; import ca.uhn.fhir.jpa.migrate.HapiMigrationException; import ca.uhn.fhir.jpa.migrate.JdbcUtils; +import ca.uhn.fhir.jpa.migrate.entity.HapiMigrationEntity; import ca.uhn.fhir.jpa.migrate.tasks.api.TaskFlagEnum; import jakarta.annotation.Nonnull; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import java.sql.SQLException; +import java.util.List; import java.util.function.Supplier; import static org.assertj.core.api.Assertions.assertThat; @@ -279,6 +281,9 @@ public void testFailureAllowed(Supplier theTestDatabaseDeta getMigrator().migrate(); assertEquals(ColumnTypeEnum.STRING, JdbcUtils.getColumnType(getConnectionProperties(), "SOMETABLE", "TEXTCOL").getColumnTypeEnum()); + List entities = myHapiMigrationDao.findAll(); + assertThat(entities).hasSize(1); + assertThat(entities.get(0).getResult()).isEqualTo(MigrationTaskExecutionResultEnum.NOT_APPLIED_ALLOWED_FAILURE.name()); } @ParameterizedTest(name = "{index}: {0}") diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/JsonParserR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/JsonParserR4Test.java index b80251d29108..27e05d93b264 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/JsonParserR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/parser/JsonParserR4Test.java @@ -7,10 +7,11 @@ import ca.uhn.fhir.model.api.annotation.DatatypeDef; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.test.BaseTest; -import ca.uhn.fhir.util.BundleBuilder; +import ca.uhn.fhir.util.ResourceUtil; import ca.uhn.fhir.util.StopWatch; import ca.uhn.fhir.util.TestUtil; import com.google.common.collect.Sets; +import jakarta.annotation.Nonnull; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.NullWriter; import org.apache.commons.lang.StringUtils; @@ -29,7 +30,6 @@ import org.hl7.fhir.r4.model.Encounter; import org.hl7.fhir.r4.model.Extension; import org.hl7.fhir.r4.model.HumanName; -import org.hl7.fhir.r4.model.IdType; import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Medication; import org.hl7.fhir.r4.model.MedicationDispense; @@ -51,18 +51,17 @@ import org.hl7.fhir.r4.model.Type; import org.hl7.fhir.r4.model.codesystems.DataAbsentReason; import org.hl7.fhir.utilities.xhtml.XhtmlNode; +import org.intellij.lang.annotations.Language; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.annotation.Nonnull; -import org.testcontainers.shaded.com.trilead.ssh2.packets.PacketDisconnect; - import java.io.IOException; -import java.sql.Ref; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; @@ -83,7 +82,7 @@ public class JsonParserR4Test extends BaseTest { private static final Logger ourLog = LoggerFactory.getLogger(JsonParserR4Test.class); - private static FhirContext ourCtx = FhirContext.forR4(); + private static final FhirContext ourCtx = FhirContext.forR4(); private Bundle createBundleWithPatient() { Bundle b = new Bundle(); @@ -101,6 +100,505 @@ private Bundle createBundleWithPatient() { @AfterEach public void afterEach() { ourCtx.getParserOptions().setAutoContainReferenceTargetsWithNoId(true); + ourCtx.setStoreRawJson(false); + } + + static List patientStrs() { + List resources = new ArrayList<>(); + + @Language("JSON") + String patientStr; + // 1 valid simple + { + patientStr = """ + { + "resourceType": "Patient", + "id": "P1212", + "contact": [{ + "name": [{ + "use": "official", + "family": "Simpson", + "given": ["Homer" ] + }] + }], + "text": { + "status": "additional", + "div": "
a div element
" + } + } + """; + } + resources.add(patientStr); + + // 2 invalid simple + { + patientStr = """ + { + "resourceType": "Patient", + "id": "P1212", + "contact": [{ + "name": [{ + "use": "official", + "family": "Simpson", + "given": ["Homer" ] + }] + }, { + "name": [{ + "use": "official", + "family": "Flanders", + "given": ["Ned"] + }] + }], + "text": { + "status": "additional", + "div": "
a div element
" + } + } + """; + } + resources.add(patientStr); + + // 3 invalid complex + { + patientStr = """ + { + "resourceType" : "Patient", + "id" : "P12312", + "meta" : { + "profile" : ["http://hl7.org/fhir/StructureDefinition/Patient"] + }, + "extension" : [ { + "url" : "http://hl7.org/fhir/StructureDefinition/us-core-ethnicity", + "extension" : [ { + "url" : "ombCategory", + "valueCoding" : { + "code" : "2186-5", + "display" : "Not Hispanic or Latino", + "system" : "urn:oid:2.16.840.1.113883.6.238" + } + }, { + "url" : "text", + "valueString" : "Non-Hisp" + } ] + }, { + "url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "extension" : [ { + "url" : "ombCategory", + "valueCoding" : { + "code" : "2054-5", + "display" : "Black or African American", + "system" : "urn:oid:2.16.840.1.113883.6.238" + } + }, { + "url" : "text", + "valueString" : "Black" + } ] + }, { + "url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", + "valueCode" : "M" + } ], + "communication" : [ { + "language" : { + "coding" : [ { + "code" : "en", + "display" : "English", + "system" : "urn:ietf:bcp:47" + }, { + "code" : "ENG", + "display" : "English", + "system" : "http://fkcfhir.org/fhir/CodeSystem/fmc-language-cs" + } ], + "text" : "EN" + }, + "preferred" : true + } ], + "telecom" : [ { + "system" : "phone", + "value" : "393-342-2312" + } ], + "identifier" : [ { + "system" : "http://hl7.org/fhir/sid/us-ssn", + "type" : { + "coding" : [ { + "system" : "http://terminology.hl7.org/CodeSystem/v2-0203", + "code" : "SS", + "display" : "Social Security Number" + } ], + "text" : "Social Security Number" + }, + "value" : "12133121" + }, { + "system" : "urn:oid:2.16.840.1.113883.3.7418.2.1", + "type" : { + "coding" : [ { + "system" : "http://terminology.hl7.org/CodeSystem/v2-0203", + "code" : "MR", + "display" : "Medical record number" + } ], + "text" : "Medical record number" + }, + "value" : "12312" + } ], + "name" : [ { + "use" : "official", + "family" : "WEIHE", + "given" : [ "FLOREZ,A" ], + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + } ], + "gender" : "male", + "birthDate" : "1955-09-19", + "active" : true, + "address" : [ { + "type" : "postal", + "line" : [ "1553 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + }, { + "type" : "physical", + "use" : "home", + "line" : [ "1553 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + } ], + "maritalStatus" : [ { + "coding" : [ { + "code" : "S", + "display" : "Never Married", + "system" : "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus" + } ], + "text" : "S" + } ], + "contact" : [ + { + "relationship" : [ { + "coding" : [ { + "code" : "PRN", + "display" : "parent", + "system" : "http://terminology.hl7.org/CodeSystem/v3-RoleCode" + } ], + "text" : "Parnt" + } ], + "name" : [ { + "use" : "official", + "family" : "PRESTIDGE", + "given" : [ "HEINEMAN" ] + } ], + "address" : [ { + "type" : "postal", + "line" : [ "1553 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + }, { + "type" : "physical", + "use" : "home", + "line" : [ "1553 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + } ], + "extension" : [ { + "url" : "http://fkcfhir.org/fhir/StructureDefinition/fmc-patient-contact-type", + "valueCodeableConcept" : { + "coding" : [ { + "system" : "http://fkcfhir.org/fhir/CodeSystem/fmc-patient-contact-type-cs", + "code" : "PRIMARY", + "display" : "Primary Contact" + } ], + "text" : "Emergency" + } + } ] + }, + { + "relationship" : [ { + "coding" : [ { + "code" : "E", + "display" : "Employer", + "system" : "http://terminology.hl7.org/CodeSystem/v2-0131" + } ], + "text" : "EMP" + } ], + "address" : [ { + "type" : "postal", + "line" : [ "1553 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + }, { + "type" : "physical", + "use" : "home", + "line" : [ "1553 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + } ], + "extension" : [ { + "url" : "http://fkcfhir.org/fhir/StructureDefinition/fmc-patient-contact-type", + "valueCodeableConcept" : { + "coding" : [ { + "system" : "http://fkcfhir.org/fhir/CodeSystem/fmc-patient-contact-type-cs", + "code" : "EMPLOYER", + "display" : "Employer" + } ] + } + }, { + "url" : "http://fkcfhir.org/fhir/StructureDefinition/fmc-patient-contact-primary-emp-ind", + "valueBoolean" : false + }, { + "url" : "http://fkcfhir.org/fhir/StructureDefinition/fmc-patient-contact-emp-status", + "valueString" : "jobStatus" + }] + } ] + } + """; + } + resources.add(patientStr); + + // 3 valid complex + { + patientStr = """ + { + "resourceType" : "Patient", + "id" : "P12312", + "meta" : { + "profile" : ["http://hl7.org/fhir/StructureDefinition/Patient"] + }, + "extension" : [ { + "url" : "http://hl7.org/fhir/StructureDefinition/us-core-ethnicity", + "extension" : [ { + "url" : "ombCategory", + "valueCoding" : { + "code" : "2186-5", + "display" : "Not Hispanic or Latino", + "system" : "urn:oid:2.16.840.1.113883.6.238" + } + }, { + "url" : "text", + "valueString" : "Non-Hisp" + } ] + }, { + "url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-race", + "extension" : [ { + "url" : "ombCategory", + "valueCoding" : { + "code" : "2054-5", + "display" : "Black or African American", + "system" : "urn:oid:2.16.840.1.113883.6.238" + } + }, { + "url" : "text", + "valueString" : "Black" + } ] + }, { + "url" : "http://hl7.org/fhir/us/core/StructureDefinition/us-core-birthsex", + "valueCode" : "M" + } ], + "communication" : [ { + "language" : { + "coding" : [ { + "code" : "en", + "display" : "English", + "system" : "urn:ietf:bcp:47" + }, { + "code" : "ENG", + "display" : "English", + "system" : "http://fkcfhir.org/fhir/CodeSystem/fmc-language-cs" + } ], + "text" : "EN" + }, + "preferred" : true + } ], + "telecom" : [ { + "system" : "phone", + "value" : "393-342-2312" + } ], + "identifier" : [ { + "system" : "http://hl7.org/fhir/sid/us-ssn", + "type" : { + "coding" : [ { + "system" : "http://terminology.hl7.org/CodeSystem/v2-0203", + "code" : "SS", + "display" : "Social Security Number" + } ], + "text" : "Social Security Number" + }, + "value" : "12133121" + }, { + "system" : "urn:oid:2.16.840.1.113883.3.7418.2.1", + "type" : { + "coding" : [ { + "system" : "http://terminology.hl7.org/CodeSystem/v2-0203", + "code" : "MR", + "display" : "Medical record number" + } ], + "text" : "Medical record number" + }, + "value" : "12312" + } ], + "name" : [ { + "use" : "official", + "family" : "WEIHE", + "given" : [ "FLOREZ,A" ], + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + } ], + "gender" : "male", + "birthDate" : "1955-09-19", + "active" : true, + "address" : [ { + "type" : "postal", + "line" : [ "1553 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + }, { + "type" : "physical", + "use" : "home", + "line" : [ "1554 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + } ], + "maritalStatus" : [ { + "coding" : [ { + "code" : "S", + "display" : "Never Married", + "system" : "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus" + } ], + "text" : "S" + } ], + "contact" : [ + { + "relationship" : [ { + "coding" : [ { + "code" : "PRN", + "display" : "parent", + "system" : "http://terminology.hl7.org/CodeSystem/v3-RoleCode" + } ], + "text" : "Parnt" + } ], + "name" : [ { + "use" : "official", + "family" : "PRESTIDGE", + "given" : [ "HEINEMAN" ] + } ], + "address" : [ { + "type" : "postal", + "line" : [ "1555 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + } ], + "extension" : [ { + "url" : "http://fkcfhir.org/fhir/StructureDefinition/fmc-patient-contact-type", + "valueCodeableConcept" : { + "coding" : [ { + "system" : "http://fkcfhir.org/fhir/CodeSystem/fmc-patient-contact-type-cs", + "code" : "PRIMARY", + "display" : "Primary Contact" + } ], + "text" : "Emergency" + } + } ] + }, + { + "relationship" : [ { + "coding" : [ { + "code" : "E", + "display" : "Employer", + "system" : "http://terminology.hl7.org/CodeSystem/v2-0131" + } ], + "text" : "EMP" + } ], + "address" : [ { + "type" : "postal", + "line" : [ "1557 SUMMIT STREET" ], + "city" : "DAVENPORT", + "state" : "IA", + "postalCode" : "52809", + "country" : "USA", + "period" : { + "start" : "2020-12-16T00:00:00-04:00" + } + } ], + "extension" : [ { + "url" : "http://fkcfhir.org/fhir/StructureDefinition/fmc-patient-contact-type", + "valueCodeableConcept" : { + "coding" : [ { + "system" : "http://fkcfhir.org/fhir/CodeSystem/fmc-patient-contact-type-cs", + "code" : "EMPLOYER", + "display" : "Employer" + } ] + } + }, { + "url" : "http://fkcfhir.org/fhir/StructureDefinition/fmc-patient-contact-primary-emp-ind", + "valueBoolean" : false + }, { + "url" : "http://fkcfhir.org/fhir/StructureDefinition/fmc-patient-contact-emp-status", + "valueString" : "jobStatus" + }] + } ] + } + """; + } + resources.add(patientStr); + + return resources; + } + + @ParameterizedTest + @MethodSource("patientStrs") + public void parseResource_withStoreRawJsonTrue_willStoreTheRawJsonOnTheResource(String thePatientStr) { + ourCtx.setStoreRawJson(true); + IParser parser = ourCtx.newJsonParser(); + + // test + Patient patient = parser.parseResource(Patient.class, thePatientStr); + + // verify + String rawJson = ResourceUtil.getRawStringFromResourceOrNull(patient); + assertEquals(thePatientStr, rawJson); } @Test diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java index 3a5454dbc5cf..4952a7beb3e3 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/validator/ValidatorWrapper.java @@ -30,6 +30,7 @@ import org.w3c.dom.NodeList; import java.io.InputStream; +import java.io.Reader; import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -148,6 +149,8 @@ public List validate( String input = theValidationContext.getResourceAsString(); EncodingEnum encoding = theValidationContext.getResourceAsStringEncoding(); + InputStream inputStream = constructNewReaderInputStream(new StringReader(input)); + if (encoding == EncodingEnum.XML) { Document document; try { @@ -167,9 +170,6 @@ public List validate( fetchAndAddProfile(theWorkerContext, profiles, nextProfileUrl, messages); } - String resourceAsString = theValidationContext.getResourceAsString(); - InputStream inputStream = new ReaderInputStream(new StringReader(resourceAsString), StandardCharsets.UTF_8); - Manager.FhirFormat format = Manager.FhirFormat.XML; v.validate(null, messages, inputStream, format, profiles); @@ -190,15 +190,12 @@ public List validate( } } - String resourceAsString = theValidationContext.getResourceAsString(); - InputStream inputStream = new ReaderInputStream(new StringReader(resourceAsString), StandardCharsets.UTF_8); - Manager.FhirFormat format = Manager.FhirFormat.JSON; v.validate(null, messages, inputStream, format, profiles); - } else { throw new IllegalArgumentException(Msg.code(649) + "Unknown encoding: " + encoding); } + // TODO: are these still needed? messages = messages.stream() .filter(m -> m.getMessageId() == null @@ -220,6 +217,19 @@ public List validate( return messages; } + private ReaderInputStream constructNewReaderInputStream(Reader theReader) { + try { + return ReaderInputStream.builder() + .setCharset(StandardCharsets.UTF_8) + .setReader(theReader) + .get(); + } catch (Exception ex) { + // we don't expect this ever + throw new IllegalArgumentException( + Msg.code(2596) + "Error constructing input reader stream while validating resource.", ex); + } + } + private void fetchAndAddProfile( IWorkerContext theWorkerContext, List theProfileStructureDefinitions, diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java index afbb4fc818f8..da6f97b31929 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/r4/validation/FhirInstanceValidatorR4Test.java @@ -1,13 +1,9 @@ package org.hl7.fhir.r4.validation; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.support.ConceptValidationOptions; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.IValidationSupport; -import ca.uhn.fhir.context.support.LookupCodeRequest; import ca.uhn.fhir.context.support.ValidationSupportContext; -import ca.uhn.fhir.context.support.ValueSetExpansionOptions; -import ca.uhn.fhir.fhirpath.BaseValidationTestWithInlineMocks; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.test.BaseTest; import ca.uhn.fhir.test.utilities.LoggingExtension; @@ -32,7 +28,6 @@ import org.hl7.fhir.common.hapi.validation.validator.FhirInstanceValidator; import org.hl7.fhir.common.hapi.validation.validator.VersionSpecificWorkerContextWrapper; import org.hl7.fhir.exceptions.FHIRException; -import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.conformance.ProfileUtilities; import org.hl7.fhir.r4.context.IWorkerContext; import org.hl7.fhir.r4.fhirpath.FHIRPathEngine; @@ -43,7 +38,6 @@ import org.hl7.fhir.r4.model.BooleanType; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; -import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.CodeType; import org.hl7.fhir.r4.model.Consent; import org.hl7.fhir.r4.model.ContactPoint; @@ -68,9 +62,7 @@ import org.hl7.fhir.r4.model.StringType; import org.hl7.fhir.r4.model.StructureDefinition; import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind; -import org.hl7.fhir.r4.model.ValueSet; import org.hl7.fhir.r4.model.ValueSet.ValueSetExpansionComponent; -import org.hl7.fhir.r4.terminologies.ValueSetExpander; import org.hl7.fhir.r5.elementmodel.JsonParser; import org.hl7.fhir.r5.test.utils.ClassesLoadedFlags; import org.hl7.fhir.r5.utils.validation.IValidationPolicyAdvisor; @@ -89,19 +81,12 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.stubbing.Answer; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.TreeSet; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; @@ -115,8 +100,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -125,7 +108,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirInstanceValidatorR4Test.class); - private static FhirContext ourCtx = FhirContext.forR4Cached(); + private static final FhirContext ourCtx = FhirContext.forR4Cached(); @RegisterExtension public LoggingExtension myLoggingExtension = new LoggingExtension(); @Mock @@ -135,7 +118,7 @@ public class FhirInstanceValidatorR4Test extends BaseTest { private FhirInstanceValidator myInstanceVal; private FhirValidator myFhirValidator; private IValidationSupport myValidationSupport; - private MockValidationSupport myMockSupport = new MockValidationSupport(FhirContext.forR4Cached()); + private final MockValidationSupport myMockSupport = new MockValidationSupport(FhirContext.forR4Cached()); /** * An invalid local reference should not cause a ServiceException.