From ee1cb4e3927a600d81a29fdad0bd3fd11d1f5430 Mon Sep 17 00:00:00 2001 From: michaelabuckley Date: Fri, 23 Sep 2022 20:47:25 -0400 Subject: [PATCH] Cleanup and utilities from composite work (#4066) Utilities and cleanup from composite work #4066 --- .../java/ca/uhn/fhir/util/ObjectUtil.java | 31 ++++- .../java/ca/uhn/fhir/util/ObjectUtilTest.java | 41 ++++-- .../java/ca/uhn/fhir/jpa/util/TestUtil.java | 11 +- .../jpa/search/builder/SearchBuilder.java | 10 +- .../fhir/jpa/dao/r4/BaseR4SearchLastN.java | 2 +- .../src/test/resources/logback-test.xml | 68 ---------- .../test/config/TestHSearchAddInConfig.java | 4 +- .../src/main/resources/logback-test.xml | 97 +++++--------- .../test/utilities/ITestDataBuilderTest.java | 123 ++++++++++++++++++ .../uhn/fhir/test/utilities/package-info.java | 4 + .../util/FhirContextSearchParamRegistry.java | 7 +- .../fhir/test/utilities/ITestDataBuilder.java | 109 +++++++++++++--- 12 files changed, 327 insertions(+), 180 deletions(-) delete mode 100644 hapi-fhir-jpaserver-elastic-test-utilities/src/test/resources/logback-test.xml create mode 100644 hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/ITestDataBuilderTest.java create mode 100644 hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/package-info.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ObjectUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ObjectUtil.java index 9e95ffc5d23..355e1b3a988 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ObjectUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/ObjectUtil.java @@ -3,6 +3,9 @@ package ca.uhn.fhir.util; import ca.uhn.fhir.i18n.Msg; import org.apache.commons.lang3.StringUtils; +import java.util.Objects; +import java.util.Optional; + /* * #%L * HAPI FHIR - Core Library @@ -25,14 +28,12 @@ import org.apache.commons.lang3.StringUtils; public class ObjectUtil { + /** + * @deprecated Just use Objects.equals() instead; + */ + @Deprecated public static boolean equals(Object object1, Object object2) { - if (object1 == object2) { - return true; - } - if ((object1 == null) || (object2 == null)) { - return false; - } - return object1.equals(object2); + return Objects.equals(object1, object2); } public static T requireNonNull(T obj, String message) { @@ -46,5 +47,21 @@ public class ObjectUtil { throw new IllegalArgumentException(Msg.code(1777) + message); } } + + /** + * Cast the object to the type using Optional. + * Useful for streaming with flatMap. + * @param theObject any object + * @param theClass the class to check instanceof + * @return Optional present if theObject is of type theClass + */ + @SuppressWarnings("unchecked") + public static Optional castIfInstanceof(Object theObject, Class theClass) { + if (theClass.isInstance(theObject)) { + return Optional.of((T) theObject); + } else { + return Optional.empty(); + } + } } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ObjectUtilTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ObjectUtilTest.java index 6e7dc389a74..73c08161ee1 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ObjectUtilTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/ObjectUtilTest.java @@ -1,16 +1,22 @@ package ca.uhn.fhir.util; -import static org.junit.jupiter.api.Assertions.*; - import ca.uhn.fhir.i18n.Msg; import org.junit.jupiter.api.Test; -public class ObjectUtilTest { +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +class ObjectUtilTest { @Test - public void testEquals() { - String a = new String("a"); - String b = new String("b"); + void testEquals() { + String a = "a"; + String b = "b"; assertFalse(ObjectUtil.equals(b, a)); assertFalse(ObjectUtil.equals(a, b)); assertFalse(ObjectUtil.equals(a, null)); @@ -20,7 +26,7 @@ public class ObjectUtilTest { } @Test - public void testRequireNonNull() { + void testRequireNonNull() { String message = "Must not be null in test"; try { ObjectUtil.requireNonNull(null, message); @@ -32,7 +38,7 @@ public class ObjectUtilTest { } @Test - public void testRequireNotEmpty() { + void testRequireNotEmpty() { //All these are empty, null or whitespace strings. testRequireNotEmptyErrorScenario(null); testRequireNotEmptyErrorScenario(""); @@ -54,5 +60,22 @@ public class ObjectUtilTest { assertEquals(Msg.code(1777) + message, e.getMessage()); } } - + + @Test + void testCast_isInstance_present() { + Boolean value = Boolean.FALSE; + + Optional result = ObjectUtil.castIfInstanceof(value, Boolean.class); + + assertTrue(result.isPresent()); + } + + @Test + void testCast_isNotInstance_empty() { + Boolean value = Boolean.FALSE; + + Optional result = ObjectUtil.castIfInstanceof(value, Integer.class); + + assertTrue(result.isEmpty()); + } } diff --git a/hapi-fhir-jpa/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java b/hapi-fhir-jpa/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java index 62c516a0f2d..d56c2193bb8 100644 --- a/hapi-fhir-jpa/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java +++ b/hapi-fhir-jpa/src/main/java/ca/uhn/fhir/jpa/util/TestUtil.java @@ -284,14 +284,13 @@ public class TestUtil { } else { Validate.notNull(fk); Validate.isTrue(isNotBlank(fk.name()), "Foreign key on " + theAnnotatedElement + " has no name()"); + + // temporarily allow the hibernate legacy sp fk names List legacySPHibernateFKNames = Arrays.asList( "FKC97MPK37OKWU8QVTCEG2NH9VN", "FKGXSREUTYMMFJUWDSWV3Y887DO"); - if (legacySPHibernateFKNames.contains(fk.name())) { - // wipmb temporarily allow the hibernate legacy sp fk names - } else { - Validate.isTrue(fk.name().startsWith("FK_"), - "Foreign key " + fk.name() + " on " + theAnnotatedElement + " must start with FK"); - } + Validate.isTrue(fk.name().startsWith("FK_") || legacySPHibernateFKNames.contains(fk.name()), + "Foreign key " + fk.name() + " on " + theAnnotatedElement + " must start with FK"); + if ( ! duplicateNameValidationExceptionList.contains(fk.name())) { assertNotADuplicateName(fk.name(), theNames); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java index cc52e42c07a..93c25c129a2 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/builder/SearchBuilder.java @@ -368,7 +368,6 @@ public class SearchBuilder implements ISearchBuilder { } if (theSearchRuntimeDetails != null) { - // wipmb we no longer have full size. theSearchRuntimeDetails.setFoundIndexMatchesCount(resultCount); HookParams params = new HookParams() .add(RequestDetails.class, theRequest) @@ -377,7 +376,7 @@ public class SearchBuilder implements ISearchBuilder { CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INDEXSEARCH_QUERY_COMPLETE, params); } - // wipmb extract + // todo MB extract this and move to FullText svc // can we skip the database entirely and return the pid list from here? boolean canSkipDatabase = // if we processed an AND clause, and it returned nothing, then nothing can match. @@ -388,12 +387,10 @@ public class SearchBuilder implements ISearchBuilder { theParams.isEmpty() && // not every param is a param. :-( theParams.getNearDistanceParam() == null && + // todo MB don't we support _lastUpdated and _offset now? theParams.getLastUpdated() == null && theParams.getEverythingMode() == null && theParams.getOffset() == null -// && -// // or sorting? -// theParams.getSort() == null ); if (canSkipDatabase) { @@ -406,7 +403,6 @@ public class SearchBuilder implements ISearchBuilder { ourLog.trace("Query needs db after HSearch. Chunking."); // Finish the query in the database for the rest of the search parameters, sorting, partitioning, etc. // We break the pids into chunks that fit in the 1k limit for jdbc bind params. - // wipmb change chunk to take iterator new QueryChunker() .chunk(Streams.stream(fulltextExecutor).collect(Collectors.toList()), t -> doCreateChunkedQueries(theParams, t, theOffset, sort, theCountOnlyFlag, theRequest, queries)); } @@ -1016,7 +1012,6 @@ public class SearchBuilder implements ISearchBuilder { if (myDaoConfig.isAdvancedHSearchIndexing() && myDaoConfig.isStoreResourceInHSearchIndex()) { List pidList = thePids.stream().map(ResourcePersistentId::getIdAsLong).collect(Collectors.toList()); - // wipmb standardize on ResourcePersistentId List resources = myFulltextSearchSvc.getResources(pidList); return resources; } else if (!Objects.isNull(myParams) && myParams.isLastN()) { @@ -1693,7 +1688,6 @@ public class SearchBuilder implements ISearchBuilder { private void initializeIteratorQuery(Integer theOffset, Integer theMaxResultsToFetch) { if (myQueryList.isEmpty()) { - // wipmb what is this? // Capture times for Lucene/Elasticsearch queries as well mySearchRuntimeDetails.setQueryStopwatch(new StopWatch()); myQueryList = createQuery(myParams, mySort, theOffset, theMaxResultsToFetch, false, myRequest, mySearchRuntimeDetails); diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseR4SearchLastN.java b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseR4SearchLastN.java index bbab42b0854..bb1cb6f8b6e 100644 --- a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseR4SearchLastN.java +++ b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/r4/BaseR4SearchLastN.java @@ -65,7 +65,7 @@ abstract public class BaseR4SearchLastN extends BaseJpaTest { private static final Map observationCategoryMap = new HashMap<>(); private static final Map observationCodeMap = new HashMap<>(); private static final Map observationEffectiveMap = new HashMap<>(); - // wipmb make thise normal fields. This static setup wasn't working + // todo mb make thise normal fields. This static setup wasn't working protected static IIdType patient0Id = null; protected static IIdType patient1Id = null; protected static IIdType patient2Id = null; diff --git a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/resources/logback-test.xml b/hapi-fhir-jpaserver-elastic-test-utilities/src/test/resources/logback-test.xml deleted file mode 100644 index 9155378c78a..00000000000 --- a/hapi-fhir-jpaserver-elastic-test-utilities/src/test/resources/logback-test.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] %msg%n - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestHSearchAddInConfig.java b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestHSearchAddInConfig.java index 6fc8ee37449..cdad79420f7 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestHSearchAddInConfig.java +++ b/hapi-fhir-jpaserver-test-utilities/src/main/java/ca/uhn/fhir/jpa/test/config/TestHSearchAddInConfig.java @@ -98,8 +98,10 @@ public class TestHSearchAddInConfig { luceneProperties.put(BackendSettings.backendKey(LuceneIndexSettings.IO_WRITER_INFOSTREAM), "true"); luceneProperties.put(HibernateOrmMapperSettings.ENABLED, "true"); - return (theProperties) -> + return (theProperties) -> { + ourLog.debug("Configuring Hibernate Search - {}", luceneProperties); theProperties.putAll(luceneProperties); + }; } diff --git a/hapi-fhir-jpaserver-test-utilities/src/main/resources/logback-test.xml b/hapi-fhir-jpaserver-test-utilities/src/main/resources/logback-test.xml index c30e58cf58d..c17cd1fc224 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/main/resources/logback-test.xml +++ b/hapi-fhir-jpaserver-test-utilities/src/main/resources/logback-test.xml @@ -5,71 +5,42 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/ITestDataBuilderTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/ITestDataBuilderTest.java new file mode 100644 index 00000000000..0bbe87617fb --- /dev/null +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/ITestDataBuilderTest.java @@ -0,0 +1,123 @@ +package ca.uhn.fhir.test.utilities; + +import ca.uhn.fhir.context.FhirContext; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.CodeableConcept; +import org.hl7.fhir.r4.model.Coding; +import org.hl7.fhir.r4.model.Observation; +import org.hl7.fhir.r4.model.Quantity; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class ITestDataBuilderTest { + + FhirContext myFhirContext = FhirContext.forR4Cached(); + + List myCreatedList = new ArrayList<>(); + List myUpdatedList = new ArrayList<>(); + + ITestDataBuilder myTDB = new ITestDataBuilder() { + @Override + public IIdType doCreateResource(IBaseResource theResource) { + myCreatedList.add(theResource); + return null; + } + + @Override + public IIdType doUpdateResource(IBaseResource theResource) { + myUpdatedList.add(theResource); + return theResource.getIdElement(); + } + + @Override + public FhirContext getFhirContext() { + return myFhirContext; + } + }; + + @Nested + class ObservationCreation { + @Test + void createObservation_withEffective_setsDate() { + myTDB.createObservation( + myTDB.withEffectiveDate("2020-01-01T12:34:56")); + + assertEquals(1, myCreatedList.size()); + Observation o = (Observation) myCreatedList.get(0); + + assertEquals("2020-01-01T12:34:56", o.getEffectiveDateTimeType().getValueAsString()); + } + + @Test + void createObservation_withObservationCode_setsCode() { + + // when + myTDB.createObservation( + myTDB.withObservationCode("http://example.com", "a-code-value", "a code description") + ); + + assertEquals(1, myCreatedList.size()); + Observation o = (Observation) myCreatedList.get(0); + + CodeableConcept codeable = o.getCode(); + assertNotNull(codeable); + assertEquals(1,codeable.getCoding().size(), "has one coding"); + Coding coding = codeable.getCoding().get(0); + + assertEquals("http://example.com", coding.getSystem()); + assertEquals("a-code-value", coding.getCode()); + assertEquals("a code description", coding.getDisplay()); + + } + + @Test + void createObservation_withValueQuantity_createsQuantity() { + myTDB.createObservation( + myTDB.withQuantityAtPath("valueQuantity", 200, "hulla", "bpm")); + + assertEquals(1, myCreatedList.size()); + Observation o = (Observation) myCreatedList.get(0); + + Quantity valueQuantity = o.getValueQuantity(); + assertNotNull(valueQuantity); + + assertEquals(200, valueQuantity.getValue().doubleValue()); + assertEquals("hulla", valueQuantity.getSystem()); + assertEquals("bpm", valueQuantity.getCode()); + } + + + @Test + void createObservation_withComponents_createsComponents() { + + // when + myTDB.createObservation( + myTDB.withObservationCode("http://example.com", "a-code-value", "a code description"), + myTDB.withEffectiveDate("2020-01-01T12:34:56"), + myTDB.withObservationComponent( + myTDB.withObservationCode("http://example.com", "another-code-value"), + myTDB.withQuantityAtPath("valueQuantity", 200, "hulla", "bpm")), + myTDB.withObservationComponent( + myTDB.withObservationCode("http://example.com", "yet-another-code-value"), + myTDB.withQuantityAtPath("valueQuantity", 1000000, "hulla", "sik")) + ); + + assertEquals(1, myCreatedList.size()); + Observation o = (Observation) myCreatedList.get(0); + + assertEquals(2, o.getComponent().size()); + Observation.ObservationComponentComponent secondComponent = o.getComponent().get(1); + + assertEquals("yet-another-code-value", secondComponent.getCode().getCoding().get(0).getCode()); + assertEquals(1000000.0, secondComponent.getValueQuantity().getValue().doubleValue()); + } + + } + +} diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/package-info.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/package-info.java new file mode 100644 index 00000000000..724f51bc1f2 --- /dev/null +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/test/utilities/package-info.java @@ -0,0 +1,4 @@ +/** + * Some test for our utilities that need a FhirContext + */ +package ca.uhn.fhir.test.utilities; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java index 2190d125917..2f741a04315 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/util/FhirContextSearchParamRegistry.java @@ -84,7 +84,12 @@ public class FhirContextSearchParamRegistry implements ISearchParamRegistry { @Nullable @Override public RuntimeSearchParam getActiveSearchParamByUrl(String theUrl) { - throw new UnsupportedOperationException(Msg.code(2067)); + // simple implementation for test support + return myCtx.getResourceTypes().stream() + .flatMap(type->getActiveSearchParams(type).values().stream()) + .filter(rsp->theUrl.equals(rsp.getUri())) + .findFirst() + .orElse(null); } @Override diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java index 0b57d425b2c..a839e9a89aa 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java @@ -26,7 +26,7 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.MetaUtil; -import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseReference; import org.hl7.fhir.instance.model.api.IBaseResource; @@ -34,6 +34,8 @@ import org.hl7.fhir.instance.model.api.ICompositeType; import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IPrimitiveType; import org.hl7.fhir.r4.model.InstantType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.util.Collection; @@ -49,6 +51,7 @@ import static org.hamcrest.Matchers.matchesPattern; */ @SuppressWarnings({"unchecked", "ConstantConditions"}) public interface ITestDataBuilder { + Logger ourLog = LoggerFactory.getLogger(ITestDataBuilder.class); /** * Set Patient.active = true @@ -106,6 +109,13 @@ public interface ITestDataBuilder { return t -> __setPrimitiveChild(getFhirContext(), t, "effectiveDateTime", "dateTime", theDate); } + /** + * Set Observation.effectiveDate + */ + default Consumer withDateTimeAt(String thePath, String theDate) { + return t -> __setPrimitiveChild(getFhirContext(), t, thePath, "dateTime", theDate); + } + /** * Set [Resource].identifier.system and [Resource].identifier.value */ @@ -192,6 +202,10 @@ public interface ITestDataBuilder { default IIdType createResource(String theResourceType, Consumer... theModifiers) { IBaseResource resource = buildResource(theResourceType, theModifiers); + if (ourLog.isDebugEnabled()) { + ourLog.debug("Creating {}", getFhirContext().newJsonParser().encodeResourceToString(resource)); + } + if (isNotBlank(resource.getIdElement().getValue())) { return doUpdateResource(resource); } else { @@ -227,15 +241,15 @@ public interface ITestDataBuilder { }; } - default Consumer withElementAt(String thePath, Consumer... theModifiers) { + default Consumer withElementAt(String thePath, Consumer... theModifiers) { return t->{ FhirTerser terser = getFhirContext().newTerser(); - IBase element = terser.addElement(t, thePath); + E element = terser.addElement(t, thePath); applyElementModifiers(element, theModifiers); }; } - default Consumer withQuantityAtPath(String thePath, double theValue, String theSystem, String theCode) { + default Consumer withQuantityAtPath(String thePath, Number theValue, String theSystem, String theCode) { return withElementAt(thePath, withPrimitiveAttribute("value", theValue), withPrimitiveAttribute("system", theSystem), @@ -256,8 +270,8 @@ public interface ITestDataBuilder { return element; } - default void applyElementModifiers(IBase element, Consumer[] theModifiers) { - for (Consumer nextModifier : theModifiers) { + default void applyElementModifiers(E element, Consumer[] theModifiers) { + for (Consumer nextModifier : theModifiers) { nextModifier.accept(element); } } @@ -266,16 +280,24 @@ public interface ITestDataBuilder { return withObservationCode(theSystem, theCode, null); } - default Consumer withObservationCode(@Nullable String theSystem, @Nullable String theCode, String theDisplay) { - return t -> { - FhirTerser terser = getFhirContext().newTerser(); - IBase coding = terser.addElement(t, "code.coding"); - terser.addElement(coding, "system", theSystem); - terser.addElement(coding, "code", theCode); - if (StringUtils.isNotEmpty(theDisplay)) { - terser.addElement(coding, "display", theDisplay); - } - }; + default Consumer withObservationCode(@Nullable String theSystem, @Nullable String theCode, @Nullable String theDisplay) { + return withCodingAt("code.coding", theSystem, theCode, theDisplay); + } + + default Consumer withCodingAt(String thePath, @Nullable String theSystem, @Nullable String theValue) { + return withCodingAt(thePath, theSystem, theValue, null); + } + + default Consumer withCodingAt(String thePath, @Nullable String theSystem, @Nullable String theValue, @Nullable String theDisplay) { + return withElementAt(thePath, + withPrimitiveAttribute("system", theSystem), + withPrimitiveAttribute("code", theValue), + withPrimitiveAttribute("display", theDisplay) + ); + } + + default Consumer withObservationComponent(Consumer... theModifiers) { + return withElementAt("component", theModifiers); } default Consumer withObservationHasMember(@Nullable IIdType theHasMember) { @@ -302,6 +324,7 @@ public interface ITestDataBuilder { }; } + // todo mb extract these to something like TestDataBuilderBacking. Maybe split out create* into child interface since people skip it. /** * Users of this API must implement this method */ @@ -328,4 +351,58 @@ public interface ITestDataBuilder { booleanType.setValueAsString(theValue); activeChild.getMutator().addValue(theTarget, booleanType); } + + interface Support { + FhirContext getFhirContext(); + IIdType createResource(IBaseResource theResource); + IIdType updateResource(IBaseResource theResource); + } + + interface WithSupport extends ITestDataBuilder { + Support getSupport(); + + @Override + default FhirContext getFhirContext() { + return getSupport().getFhirContext(); + } + + @Override + default IIdType doCreateResource(IBaseResource theResource) { + return getSupport().createResource(theResource); + } + + @Override + default IIdType doUpdateResource(IBaseResource theResource) { + return getSupport().updateResource(theResource); + } + } + + /** + * Dummy support to use ITestDataBuilder as just a builder, not a DAO + * todo mb Maybe we should split out the builder into a super-interface and drop this? + */ + class SupportNoDao implements Support { + final FhirContext myFhirContext; + + public SupportNoDao(FhirContext theFhirContext) { + myFhirContext = theFhirContext; + } + + @Override + public FhirContext getFhirContext() { + return myFhirContext; + } + + @Override + public IIdType createResource(IBaseResource theResource) { + Validate.isTrue(false, "Create not supported"); + return null; + } + + @Override + public IIdType updateResource(IBaseResource theResource) { + Validate.isTrue(false, "Update not supported"); + return null; + } + } }