From d86e9d28b8abf5e7d66fae828374807c2d3fe322 Mon Sep 17 00:00:00 2001 From: Troy Biesterfeld Date: Fri, 19 Mar 2021 11:31:30 -0500 Subject: [PATCH 1/5] Issue #1323 - Extract :of-type parameter as internal composite Signed-off-by: Troy Biesterfeld --- .../jdbc/impl/FHIRPersistenceJDBCImpl.java | 6 +- .../util/JDBCParameterBuildingVisitor.java | 91 ++++++++++++++-- .../test/util/ParameterExtractionTest.java | 103 ++++++++++++------ .../com/ibm/fhir/search/SearchConstants.java | 7 +- 4 files changed, 156 insertions(+), 51 deletions(-) diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/impl/FHIRPersistenceJDBCImpl.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/impl/FHIRPersistenceJDBCImpl.java index 53e5b87e580..2333261de50 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/impl/FHIRPersistenceJDBCImpl.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/impl/FHIRPersistenceJDBCImpl.java @@ -1409,7 +1409,7 @@ private List extractSearchParameters(Resource fhirResou // Of course, that would require adding extension-search-params to the Registry which would require the Registry to be tenant-aware. // SearchParameter compSP = FHIRRegistry.getInstance().getResource(component.getDefinition().getValue(), SearchParameter.class); SearchParameter compSP = SearchUtil.getSearchParameter(p.getResourceType(), component.getDefinition()); - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(compSP); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(p.getResourceType(), compSP); FHIRPathNode node = nodes.iterator().next(); if (nodes.size() > 1 ) { // TODO: support component expressions that result in multiple nodes @@ -1455,7 +1455,6 @@ private List extractSearchParameters(Resource fhirResou ExtractedParameterValue componentParam = parameters.get(0); // override the component parameter name with the composite parameter name componentParam.setName(SearchUtil.makeCompositeSubCode(code, componentParam.getName())); - componentParam.setResourceType(p.getResourceType()); componentParam.setBase(p.getBase()); p.addComponent(componentParam); } else if (node.isSystemValue()){ @@ -1498,7 +1497,7 @@ private List extractSearchParameters(Resource fhirResou } } } else { // ! SearchParamType.COMPOSITE.equals(sp.getType()) - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(sp); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(fhirResource.getClass().getSimpleName(), sp); for (FHIRPathNode value : values) { @@ -1542,7 +1541,6 @@ private List extractSearchParameters(Resource fhirResou // retrieve the list of parameters built from all the FHIRPathElementNode values List parameters = parameterBuilder.getResult(); for (ExtractedParameterValue p : parameters) { - p.setResourceType(fhirResource.getClass().getSimpleName()); allParameters.add(p); if (log.isLoggable(Level.FINE)) { log.fine("Extracted Parameter '" + p.getName() + "' from Resource."); diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/JDBCParameterBuildingVisitor.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/JDBCParameterBuildingVisitor.java index 7846623c64a..38c09556204 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/JDBCParameterBuildingVisitor.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/util/JDBCParameterBuildingVisitor.java @@ -47,6 +47,7 @@ import com.ibm.fhir.model.util.ModelSupport; import com.ibm.fhir.model.visitor.DefaultVisitor; import com.ibm.fhir.model.visitor.Visitable; +import com.ibm.fhir.persistence.jdbc.dto.CompositeParmVal; import com.ibm.fhir.persistence.jdbc.dto.DateParmVal; import com.ibm.fhir.persistence.jdbc.dto.ExtractedParameterValue; import com.ibm.fhir.persistence.jdbc.dto.LocationParmVal; @@ -62,6 +63,7 @@ import com.ibm.fhir.search.util.ReferenceUtil; import com.ibm.fhir.search.util.ReferenceValue; import com.ibm.fhir.search.util.ReferenceValue.ReferenceType; +import com.ibm.fhir.search.util.SearchUtil; /** * This class is the JDBC persistence layer implementation for transforming @@ -90,6 +92,7 @@ public class JDBCParameterBuildingVisitor extends DefaultVisitor { private static final Timestamp LARGEST_TIMESTAMP = Timestamp.from( ZonedDateTime.parse("9999-12-31T23:59:59.999999Z").toInstant()); + private final String resourceType; // We only need the SearchParameter type and code, so just store those directly as members private final String searchParamCode; private final SearchParamType searchParamType; @@ -99,8 +102,9 @@ public class JDBCParameterBuildingVisitor extends DefaultVisitor { */ private List result; - public JDBCParameterBuildingVisitor(SearchParameter searchParameter) { + public JDBCParameterBuildingVisitor(String resourceType, SearchParameter searchParameter) { super(false); + this.resourceType = resourceType; this.searchParamCode = searchParameter.getCode().getValue(); this.searchParamType = searchParameter.getType(); @@ -131,10 +135,11 @@ public boolean visit(String elementName, int elementIndex, Visitable visitable) @Override public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhir.model.type.Boolean _boolean) { if (_boolean.hasValue()) { - TokenParmVal p = new TokenParmVal(); if (!TOKEN.equals(searchParamType)) { throw invalidComboException(searchParamType, _boolean); } + TokenParmVal p = new TokenParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueSystem("http://terminology.hl7.org/CodeSystem/special-values"); if (_boolean.getValue()) { @@ -150,10 +155,11 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi @Override public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhir.model.type.Canonical canonical) { if (canonical.hasValue()) { - StringParmVal p = new StringParmVal(); if (!REFERENCE.equals(searchParamType) && !URI.equals(searchParamType)) { throw invalidComboException(searchParamType, canonical); } + StringParmVal p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(canonical.getValue()); result.add(p); @@ -164,10 +170,11 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi @Override public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhir.model.type.Code code) { if (code.hasValue()) { - TokenParmVal p = new TokenParmVal(); if (!TOKEN.equals(searchParamType)) { throw invalidComboException(searchParamType, code); } + TokenParmVal p = new TokenParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueSystem(ModelSupport.getSystem(code)); p.setValueCode(code.getValue()); @@ -179,10 +186,11 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi @Override public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhir.model.type.Date date) { if (date.hasValue()) { - DateParmVal p = new DateParmVal(); if (!DATE.equals(searchParamType)) { throw invalidComboException(searchParamType, date); } + DateParmVal p = new DateParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); setDateValues(p, date); result.add(p); @@ -193,10 +201,11 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi @Override public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhir.model.type.DateTime dateTime) { if (dateTime.hasValue()) { - DateParmVal p = new DateParmVal(); if (!DATE.equals(searchParamType)) { throw invalidComboException(searchParamType, dateTime); } + DateParmVal p = new DateParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); setDateValues(p, dateTime); result.add(p); @@ -207,10 +216,11 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi @Override public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhir.model.type.Decimal decimal) { if (decimal.hasValue()) { - NumberParmVal p = new NumberParmVal(); if (!NUMBER.equals(searchParamType)) { throw invalidComboException(searchParamType, decimal); } + NumberParmVal p = new NumberParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); BigDecimal value = decimal.getValue(); p.setValueNumber(value); @@ -224,10 +234,11 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi @Override public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhir.model.type.Id id) { if (id.hasValue()) { - TokenParmVal p = new TokenParmVal(); if (!TOKEN.equals(searchParamType)) { throw invalidComboException(searchParamType, id); } + TokenParmVal p = new TokenParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueCode(id.getValue()); result.add(p); @@ -238,10 +249,11 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi @Override public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhir.model.type.Instant instant) { if (instant.hasValue()) { - DateParmVal p = new DateParmVal(); if (!DATE.equals(searchParamType)) { throw invalidComboException(searchParamType, instant); } + DateParmVal p = new DateParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); Timestamp t = generateTimestamp(instant.getValue().toInstant()); p.setValueDateStart(t); @@ -254,10 +266,11 @@ public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhi @Override public boolean visit(java.lang.String elementName, int elementIndex, com.ibm.fhir.model.type.Integer integer) { if (integer.hasValue()) { - NumberParmVal p = new NumberParmVal(); if (!NUMBER.equals(searchParamType)) { throw invalidComboException(searchParamType, integer); } + NumberParmVal p = new NumberParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); BigDecimal value = new BigDecimal(integer.getValue()); p.setValueNumber(value); @@ -273,11 +286,13 @@ public boolean visit(String elementName, int elementIndex, com.ibm.fhir.model.ty if (value.hasValue()) { if (STRING.equals(searchParamType)) { StringParmVal p = new StringParmVal(); + p.setResourceType(resourceType); p.setValueString(value.getValue()); p.setName(searchParamCode); result.add(p); } else if (TOKEN.equals(searchParamType)) { TokenParmVal p = new TokenParmVal(); + p.setResourceType(resourceType); p.setValueCode(value.getValue()); p.setName(searchParamCode); result.add(p); @@ -299,11 +314,13 @@ public boolean visit(String elementName, int elementIndex, Uri uri) { // not strings. if (REFERENCE.equals(this.searchParamType)) { TokenParmVal p = new TokenParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueCode(uri.getValue()); result.add(p); } else { StringParmVal p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(uri.getValue()); result.add(p); @@ -326,42 +343,49 @@ public boolean visit(java.lang.String elementName, int elementIndex, Address add } for (com.ibm.fhir.model.type.String aLine : address.getLine()) { p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(aLine.getValue()); result.add(p); } if (address.getCity() != null) { p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(address.getCity().getValue()); result.add(p); } if (address.getDistrict() != null) { p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(address.getDistrict().getValue()); result.add(p); } if (address.getState() != null) { p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(address.getState().getValue()); result.add(p); } if (address.getCountry() != null) { p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(address.getCountry().getValue()); result.add(p); } if (address.getPostalCode() != null) { p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(address.getPostalCode().getValue()); result.add(p); } if (address.getText() != null) { p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(address.getText().getValue()); result.add(p); @@ -383,10 +407,11 @@ public boolean visit(java.lang.String elementName, int elementIndex, CodeableCon @Override public boolean visit(java.lang.String elementName, int elementIndex, Coding coding) { if (coding.getCode() != null && coding.getCode().hasValue()) { - TokenParmVal p = new TokenParmVal(); if (!TOKEN.equals(searchParamType)) { throw invalidComboException(searchParamType, coding); } + TokenParmVal p = new TokenParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueCode(coding.getCode().getValue()); if (coding.getSystem() != null) { @@ -404,6 +429,7 @@ public boolean visit(java.lang.String elementName, int elementIndex, ContactPoin throw invalidComboException(searchParamType, contactPoint); } TokenParmVal telecom = new TokenParmVal(); + telecom.setResourceType(resourceType); telecom.setName(searchParamCode); telecom.setValueCode(contactPoint.getValue().getValue()); result.add(telecom); @@ -420,30 +446,35 @@ public boolean visit(java.lang.String elementName, int elementIndex, HumanName h if (humanName.getFamily() != null) { // family is just a string in R4 (not a list) p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(humanName.getFamily().getValue()); result.add(p); } for (com.ibm.fhir.model.type.String given : humanName.getGiven()) { p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(given.getValue()); result.add(p); } for (com.ibm.fhir.model.type.String prefix : humanName.getPrefix()) { p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(prefix.getValue()); result.add(p); } for (com.ibm.fhir.model.type.String suffix : humanName.getSuffix()) { p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(suffix.getValue()); result.add(p); } if (humanName.getText() != null) { p = new StringParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueString(humanName.getText().getValue()); result.add(p); @@ -458,6 +489,7 @@ public boolean visit(java.lang.String elementName, int elementIndex, Money money } if (money != null && money.getValue() != null && money.getValue().getValue() != null) { QuantityParmVal p = new QuantityParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueNumber(money.getValue().getValue()); if (money.getCurrency() != null) { @@ -478,6 +510,7 @@ public boolean visit(java.lang.String elementName, int elementIndex, Period peri return false; } DateParmVal p = new DateParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); if (period.getStart() == null || period.getStart().getValue() == null) { p.setValueDateStart(SMALLEST_TIMESTAMP); @@ -510,6 +543,7 @@ public boolean visit(java.lang.String elementName, int elementIndex, Quantity qu // see https://gforge.hl7.org/gf/project/fhir/tracker/?action=TrackerItemEdit&tracker_item_id=19597 if (quantity.getCode() != null && quantity.getCode().hasValue()) { QuantityParmVal p = new QuantityParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueNumber(value); p.setValueNumberLow(valueLow); @@ -525,6 +559,7 @@ public boolean visit(java.lang.String elementName, int elementIndex, Quantity qu // No need to save a second parameter value if the display unit matches the coded unit if (quantity.getCode() == null || !displayUnit.equals(quantity.getCode().getValue())) { QuantityParmVal p = new QuantityParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); p.setValueNumber(value); p.setValueNumberLow(valueLow); @@ -544,6 +579,7 @@ public boolean visit(java.lang.String elementName, int elementIndex, Range range } // The parameter isn't added unless either low or high holds a value QuantityParmVal p = new QuantityParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); if (range.getLow() != null && range.getLow().getValue() != null && range.getLow().getValue().getValue() != null) { @@ -587,12 +623,42 @@ public boolean visit(java.lang.String elementName, int elementIndex, Identifier } if (identifier != null && identifier.getValue() != null) { TokenParmVal p = new TokenParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); if (identifier.getSystem() != null) { p.setValueSystem(identifier.getSystem().getValue()); } p.setValueCode(identifier.getValue().getValue()); result.add(p); + if (identifier.getType() != null) { + for (Coding typeCoding : identifier.getType().getCoding()) { + if (typeCoding.getCode() != null && typeCoding.getCode().hasValue()) { + // Create as composite parm since type/value are correlated for the :of-type modifier + CompositeParmVal cp = new CompositeParmVal(); + cp.setResourceType(resourceType); + cp.setName(searchParamCode + SearchConstants.OF_TYPE_MODIFIER_SUFFIX); + + // type + p = new TokenParmVal(); + p.setResourceType(cp.getResourceType()); + p.setName(SearchUtil.makeCompositeSubCode(cp.getName(), SearchConstants.OF_TYPE_MODIFIER_COMPONENT_TYPE)); + if (typeCoding.getSystem() != null) { + p.setValueSystem(typeCoding.getSystem().getValue()); + } + p.setValueCode(typeCoding.getCode().getValue()); + cp.addComponent(p); + + // value + p = new TokenParmVal(); + p.setResourceType(cp.getResourceType()); + p.setName(SearchUtil.makeCompositeSubCode(cp.getName(), SearchConstants.OF_TYPE_MODIFIER_COMPONENT_VALUE)); + p.setValueCode(identifier.getValue().getValue()); + cp.addComponent(p); + + result.add(cp); + } + } + } } return false; } @@ -611,6 +677,7 @@ public boolean visit(java.lang.String elementName, int elementIndex, Reference r ReferenceValue refValue = ReferenceUtil.createReferenceValueFrom(reference, baseUrl); if (refValue.getType() != ReferenceType.LOGICAL && refValue.getType() != ReferenceType.INVALID && refValue.getType() != ReferenceType.DISPLAY_ONLY) { ReferenceParmVal p = new ReferenceParmVal(); + p.setResourceType(resourceType); p.setRefValue(refValue); p.setName(searchParamCode); result.add(p); @@ -618,6 +685,7 @@ public boolean visit(java.lang.String elementName, int elementIndex, Reference r Identifier identifier = reference.getIdentifier(); if (identifier != null && identifier.getValue() != null) { TokenParmVal p = new TokenParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode + SearchConstants.IDENTIFIER_MODIFIER_SUFFIX); if (identifier.getSystem() != null) { p.setValueSystem(identifier.getSystem().getValue()); @@ -660,6 +728,7 @@ public boolean visit(java.lang.String elementName, int elementIndex, Timing timi @Override public boolean visit(java.lang.String elementName, int elementIndex, Location.Position position) { LocationParmVal p = new LocationParmVal(); + p.setResourceType(resourceType); p.setName(searchParamCode); // The following code ensures that the lat/lon is only added when there is a pair. diff --git a/fhir-persistence-jdbc/src/test/java/com/ibm/fhir/persistence/jdbc/test/util/ParameterExtractionTest.java b/fhir-persistence-jdbc/src/test/java/com/ibm/fhir/persistence/jdbc/test/util/ParameterExtractionTest.java index 0c24951802e..32862a1a8d9 100644 --- a/fhir-persistence-jdbc/src/test/java/com/ibm/fhir/persistence/jdbc/test/util/ParameterExtractionTest.java +++ b/fhir-persistence-jdbc/src/test/java/com/ibm/fhir/persistence/jdbc/test/util/ParameterExtractionTest.java @@ -56,6 +56,7 @@ import com.ibm.fhir.model.type.code.ResourceType; import com.ibm.fhir.model.type.code.SearchParamType; import com.ibm.fhir.persistence.exception.FHIRPersistenceProcessorException; +import com.ibm.fhir.persistence.jdbc.dto.CompositeParmVal; import com.ibm.fhir.persistence.jdbc.dto.DateParmVal; import com.ibm.fhir.persistence.jdbc.dto.ExtractedParameterValue; import com.ibm.fhir.persistence.jdbc.dto.NumberParmVal; @@ -65,6 +66,7 @@ import com.ibm.fhir.persistence.jdbc.dto.TokenParmVal; import com.ibm.fhir.persistence.jdbc.util.JDBCParameterBuildingVisitor; import com.ibm.fhir.search.SearchConstants; +import com.ibm.fhir.search.util.SearchUtil; /** * Tests all valid combinations of search paramter types and data types @@ -113,7 +115,7 @@ public void setSystemTimeZone() { @Test public void testBoolean() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(tokenSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, tokenSearchParam); com.ibm.fhir.model.type.Boolean.TRUE.accept(parameterBuilder); List params = parameterBuilder.getResult(); assertEquals(params.size(), 1, "Number of extracted parameters"); @@ -133,13 +135,13 @@ public void testCanonical() throws FHIRPersistenceProcessorException { Canonical canonical = Canonical.of(SAMPLE_URI); List params; - parameterBuilder = new JDBCParameterBuildingVisitor(referenceSearchParam); + parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, referenceSearchParam); canonical.accept(parameterBuilder); params = parameterBuilder.getResult(); assertEquals(params.size(), 1, "Number of extracted parameters"); assertEquals(((StringParmVal) params.get(0)).getValueString(), SAMPLE_URI); - parameterBuilder = new JDBCParameterBuildingVisitor(uriSearchParam); + parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, uriSearchParam); canonical.accept(parameterBuilder); params = parameterBuilder.getResult(); assertEquals(params.size(), 1, "Number of extracted parameters"); @@ -154,7 +156,7 @@ public void testCanonical_null() throws FHIRPersistenceProcessorException { @Test public void testCode() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(tokenSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, tokenSearchParam); Code.of(SAMPLE_STRING).accept(parameterBuilder); List params = parameterBuilder.getResult(); assertEquals(params.size(), 1, "Number of extracted parameters"); @@ -168,7 +170,7 @@ public void testCode_null() throws FHIRPersistenceProcessorException { @Test public void testDate() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(dateSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, dateSearchParam); Date.of("2016").accept(parameterBuilder); List params = parameterBuilder.getResult(); for (ExtractedParameterValue param : params) { @@ -185,7 +187,7 @@ public void testDate_null() throws FHIRPersistenceProcessorException { @Test public void testDateTime() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(dateSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, dateSearchParam); DateTime.of("2016-01-01T10:10:10.1+04:00").accept(parameterBuilder); List params = parameterBuilder.getResult(); for (ExtractedParameterValue param : params) { @@ -200,7 +202,7 @@ public void testDateTime_null() throws FHIRPersistenceProcessorException { @Test public void testDecimal() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(numberSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, numberSearchParam); Decimal.of(99.99).accept(parameterBuilder); List params = parameterBuilder.getResult(); assertEquals(params.size(), 1, "Number of extracted parameters"); @@ -216,7 +218,7 @@ public void testDecimal_null() throws FHIRPersistenceProcessorException { @Test public void testId() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(tokenSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, tokenSearchParam); Id.of("x").accept(parameterBuilder); List params = parameterBuilder.getResult(); assertEquals(params.size(), 1, "Number of extracted parameters"); @@ -230,7 +232,7 @@ public void testId_null() throws FHIRPersistenceProcessorException { @Test public void testInstant() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(dateSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, dateSearchParam); Instant now = Instant.now(ZoneOffset.UTC); now.accept(parameterBuilder); List params = parameterBuilder.getResult(); @@ -245,7 +247,7 @@ public void testInstant_null() throws FHIRPersistenceProcessorException { @Test public void testInteger() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(numberSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, numberSearchParam); Integer.of(13).accept(parameterBuilder); List params = parameterBuilder.getResult(); assertEquals(params.size(), 1, "Number of extracted parameters"); @@ -263,13 +265,13 @@ public void testString() throws FHIRPersistenceProcessorException { com.ibm.fhir.model.type.String stringVal = string(SAMPLE_STRING); List params; - parameterBuilder = new JDBCParameterBuildingVisitor(stringSearchParam); + parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, stringSearchParam); stringVal.accept(parameterBuilder); params = parameterBuilder.getResult(); assertEquals(params.size(), 1, "Number of extracted parameters"); assertEquals(((StringParmVal) params.get(0)).getValueString(), SAMPLE_STRING); - parameterBuilder = new JDBCParameterBuildingVisitor(tokenSearchParam); + parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, tokenSearchParam); stringVal.accept(parameterBuilder); params = parameterBuilder.getResult(); assertEquals(params.size(), 1, "Number of extracted parameters"); @@ -288,13 +290,13 @@ public void testUri() throws FHIRPersistenceProcessorException { Uri uri = Uri.of(SAMPLE_URI); List params; - parameterBuilder = new JDBCParameterBuildingVisitor(referenceSearchParam); + parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, referenceSearchParam); uri.accept(parameterBuilder); params = parameterBuilder.getResult(); assertEquals(params.size(), 1, "Number of extracted parameters"); assertEquals(((TokenParmVal) params.get(0)).getValueCode(), SAMPLE_URI); - parameterBuilder = new JDBCParameterBuildingVisitor(uriSearchParam); + parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, uriSearchParam); uri.accept(parameterBuilder); params = parameterBuilder.getResult(); assertEquals(params.size(), 1, "Number of extracted parameters"); @@ -308,7 +310,7 @@ public void testUri_null() throws FHIRPersistenceProcessorException { } private void assertNullValueReturnsNoParameters(SearchParameter sp, Element.Builder builder) { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(sp); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, sp); builder.extension(SAMPLE_EXTENSION).build().accept(parameterBuilder); List params = parameterBuilder.getResult(); assertEquals(params.size(), 0, "Number of extracted parameters"); @@ -317,7 +319,7 @@ private void assertNullValueReturnsNoParameters(SearchParameter sp, Element.Buil @Test public void testAddress() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(stringSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, stringSearchParam); Address.builder() .line(string("4025 S. Miami Blvd.")) //0 .city(string("Durham")) //1 @@ -342,7 +344,7 @@ public void testAddress_null() throws FHIRPersistenceProcessorException { @Test public void testAge() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(quantitySearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, quantitySearchParam); Age.builder() .value(Decimal.of(1)) .system(Uri.of(UNITSOFMEASURE)) @@ -365,7 +367,7 @@ public void testAge_null() throws FHIRPersistenceProcessorException { @Test public void testCodeableConcept() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(tokenSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, tokenSearchParam); CodeableConcept.builder() .coding(Coding.builder().code(Code.of("a")).system(Uri.of(SAMPLE_URI)).build()) .coding(Coding.builder().code(Code.of("b")).system(Uri.of(SAMPLE_URI)).build()) @@ -389,7 +391,7 @@ public void testCodeableConcept_null() throws FHIRPersistenceProcessorException @Test public void testCoding() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(tokenSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, tokenSearchParam); Coding.builder() .code(Code.of(SAMPLE_STRING)) .system(Uri.of(SAMPLE_URI)) @@ -408,7 +410,7 @@ public void testCoding_null() throws FHIRPersistenceProcessorException { @Test public void testContactPoint() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(tokenSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, tokenSearchParam); ContactPoint.builder() .system(ContactPointSystem.PHONE) .value(string("5558675309")) @@ -426,7 +428,7 @@ public void testContactPoint_null() throws FHIRPersistenceProcessorException { @Test public void testDuration() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(quantitySearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, quantitySearchParam); Duration.builder() .value(Decimal.of(1)) .system(Uri.of(UNITSOFMEASURE)) @@ -447,7 +449,7 @@ public void testDuration_null() throws FHIRPersistenceProcessorException { @Test public void testHumanName() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(stringSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, stringSearchParam); HumanName.builder() .family(string("Simpson")) //0 .given(string("Nick")) //1 @@ -472,16 +474,47 @@ public void testHumanName_null() throws FHIRPersistenceProcessorException { @Test public void testIdentifier() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(tokenSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, tokenSearchParam); Identifier.builder() + .type(CodeableConcept.builder() + .coding(Coding.builder().code(Code.of("codea")).system(Uri.of("systema")).build()) + .coding(Coding.builder().code(Code.of("codeb")).build()) + .build()) .system(Uri.of(SAMPLE_URI)) .value(string("abc123")) .build() .accept(parameterBuilder); List params = parameterBuilder.getResult(); - assertEquals(params.size(), 1, "Number of extracted parameters"); + assertEquals(params.size(), 3, "Number of extracted parameters"); assertEquals(((TokenParmVal) params.get(0)).getValueSystem(), SAMPLE_URI); assertEquals(((TokenParmVal) params.get(0)).getValueCode(), "abc123"); + + // Check composite parameters extracted for :of-type modifier + String compositeCode = SEARCH_PARAM_CODE_VALUE + SearchConstants.OF_TYPE_MODIFIER_SUFFIX; + + CompositeParmVal cParmVal = (CompositeParmVal) params.get(1); + assertEquals(cParmVal.getName(), compositeCode); + assertEquals(cParmVal.getComponent().size(), 2, "Number of extracted components"); + TokenParmVal tokenParmVal = (TokenParmVal) cParmVal.getComponent().get(0); + assertEquals(tokenParmVal.getName(), SearchUtil.makeCompositeSubCode(compositeCode, SearchConstants.OF_TYPE_MODIFIER_COMPONENT_TYPE)); + assertEquals(tokenParmVal.getValueSystem(), "systema"); + assertEquals(tokenParmVal.getValueCode(), "codea"); + tokenParmVal = (TokenParmVal) cParmVal.getComponent().get(1); + assertEquals(tokenParmVal.getName(), SearchUtil.makeCompositeSubCode(compositeCode, SearchConstants.OF_TYPE_MODIFIER_COMPONENT_VALUE)); + assertEquals(tokenParmVal.getValueSystem(), TokenParmVal.DEFAULT_TOKEN_SYSTEM); + assertEquals(tokenParmVal.getValueCode(), "abc123"); + + cParmVal = (CompositeParmVal) params.get(2); + assertEquals(cParmVal.getName(), compositeCode); + assertEquals(cParmVal.getComponent().size(), 2, "Number of extracted components"); + tokenParmVal = (TokenParmVal) cParmVal.getComponent().get(0); + assertEquals(tokenParmVal.getName(), SearchUtil.makeCompositeSubCode(compositeCode, SearchConstants.OF_TYPE_MODIFIER_COMPONENT_TYPE)); + assertEquals(tokenParmVal.getValueSystem(), TokenParmVal.DEFAULT_TOKEN_SYSTEM); + assertEquals(tokenParmVal.getValueCode(), "codeb"); + tokenParmVal = (TokenParmVal) cParmVal.getComponent().get(1); + assertEquals(tokenParmVal.getName(), SearchUtil.makeCompositeSubCode(compositeCode, SearchConstants.OF_TYPE_MODIFIER_COMPONENT_VALUE)); + assertEquals(tokenParmVal.getValueSystem(), TokenParmVal.DEFAULT_TOKEN_SYSTEM); + assertEquals(tokenParmVal.getValueCode(), "abc123"); } @Test @@ -491,7 +524,7 @@ public void testIdentifier_null() throws FHIRPersistenceProcessorException { @Test public void testMoney() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(quantitySearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, quantitySearchParam); Money.builder() .currency(Code.of("USD")) .value(Decimal.of(100)) @@ -510,7 +543,7 @@ public void testMoney_null() throws FHIRPersistenceProcessorException { @Test public void testPeriod() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(dateSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, dateSearchParam); Period.builder() .start(DateTime.of(SAMPLE_DATE_START)) .end(DateTime.of(SAMPLE_DATE_END)) @@ -524,7 +557,7 @@ public void testPeriod() throws FHIRPersistenceProcessorException { @Test public void testPeriod_nullStart() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(dateSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, dateSearchParam); Period.builder() .end(DateTime.of(SAMPLE_DATE_END)) .build() @@ -536,7 +569,7 @@ public void testPeriod_nullStart() throws FHIRPersistenceProcessorException { @Test public void testPeriod_nullEnd() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(dateSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, dateSearchParam); Period.builder() .start(DateTime.of(SAMPLE_DATE_START)) .build() @@ -553,7 +586,7 @@ public void testPeriod_null() throws FHIRPersistenceProcessorException { @Test public void testQuantity() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(quantitySearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, quantitySearchParam); Quantity.builder() .value(Decimal.of(1)) .system(Uri.of(UNITSOFMEASURE)) @@ -574,7 +607,7 @@ public void testQuantity_null() throws FHIRPersistenceProcessorException { @Test public void testRange() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(quantitySearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, quantitySearchParam); Range range = Range.builder() .low(SimpleQuantity.builder() .code(Code.of(SAMPLE_UNIT)) @@ -601,7 +634,7 @@ public void testRange() throws FHIRPersistenceProcessorException { @Test public void testRange_nullHigh() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(quantitySearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, quantitySearchParam); Range range = Range.builder() .low(SimpleQuantity.builder() .code(Code.of(SAMPLE_UNIT)) @@ -622,7 +655,7 @@ public void testRange_nullHigh() throws FHIRPersistenceProcessorException { @Test public void testRange_nullLow() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(quantitySearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, quantitySearchParam); Range range = Range.builder() .high(SimpleQuantity.builder() .code(Code.of(SAMPLE_UNIT)) @@ -648,7 +681,7 @@ public void testRange_null() throws FHIRPersistenceProcessorException { @Test public void testReference() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(referenceSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, referenceSearchParam); Reference.builder() .reference(string(SAMPLE_REF)) .identifier(Identifier.builder() @@ -674,7 +707,7 @@ public void testReference_null() throws FHIRPersistenceProcessorException { @Test public void testTimingBounds() throws FHIRPersistenceProcessorException { - JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(dateSearchParam); + JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, dateSearchParam); Period period = Period.builder() .start(DateTime.of(SAMPLE_DATE_START)) .end(DateTime.of(SAMPLE_DATE_END)) @@ -698,7 +731,7 @@ public void testTiming_null() throws FHIRPersistenceProcessorException { // Timing doesn't currently extract from "events" // @Test // public void testTimingEvents() throws FHIRPersistenceProcessorException { -// JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(dateSearchParam); +// JDBCParameterBuildingVisitor parameterBuilder = new JDBCParameterBuildingVisitor(SAMPLE_REF_RESOURCE_TYPE, dateSearchParam); // Timing.builder() // .event(DateTime.of(SAMPLE_DATE_START)) // .event(DateTime.of(SAMPLE_DATE_END)) diff --git a/fhir-search/src/main/java/com/ibm/fhir/search/SearchConstants.java b/fhir-search/src/main/java/com/ibm/fhir/search/SearchConstants.java index debbd1698e4..7461567f3d8 100644 --- a/fhir-search/src/main/java/com/ibm/fhir/search/SearchConstants.java +++ b/fhir-search/src/main/java/com/ibm/fhir/search/SearchConstants.java @@ -99,7 +99,12 @@ private SearchConstants() { // Extracted search parameter suffix for :identifier modifier public static final String IDENTIFIER_MODIFIER_SUFFIX = ":identifier"; - + + // Extracted search parameter suffixes for :of-type modifier + public static final String OF_TYPE_MODIFIER_SUFFIX = ":of-type"; + public static final String OF_TYPE_MODIFIER_COMPONENT_TYPE = "type"; + public static final String OF_TYPE_MODIFIER_COMPONENT_VALUE = "value"; + // set as unmodifiable public static final Set SEARCH_RESULT_PARAMETER_NAMES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(SORT, COUNT, PAGE, INCLUDE, REVINCLUDE, ELEMENTS, SUMMARY, TOTAL))); From 2d3af5f4a725991cb8de06ba46e057fcd8fa7eb2 Mon Sep 17 00:00:00 2001 From: Troy Biesterfeld Date: Fri, 19 Mar 2021 16:54:08 -0500 Subject: [PATCH 2/5] Issue #1323 - Add support for ':of-type' modifier Signed-off-by: Troy Biesterfeld --- docs/src/pages/Conformance.md | 6 +- .../fhir/persistence/jdbc/JDBCConstants.java | 2 +- .../com/ibm/fhir/search/SearchConstants.java | 2 + .../parameters/QueryParameterValue.java | 20 ++- .../com/ibm/fhir/search/util/SearchUtil.java | 44 ++++++- .../com/ibm/fhir/server/test/SearchTest.java | 117 ++++++++++++++++++ 6 files changed, 180 insertions(+), 11 deletions(-) diff --git a/docs/src/pages/Conformance.md b/docs/src/pages/Conformance.md index 6556bf1363b..ef67f307e76 100644 --- a/docs/src/pages/Conformance.md +++ b/docs/src/pages/Conformance.md @@ -2,7 +2,7 @@ layout: post title: Conformance description: Notes on the Conformance of the IBM FHIR Server -date: 2021-03-17 12:00:00 -0400 +date: 2021-03-19 12:00:00 -0400 permalink: /conformance/ --- @@ -165,7 +165,7 @@ FHIR search modifiers are described at https://www.hl7.org/fhir/R4/search.html#m |String |`:exact`,`:contains`,`:missing` |"starts with" search that is case-insensitive and accent-insensitive| |Reference |`:[type]`,`:missing`,`:identifier` |exact match search and targets are implicitly added| |URI |`:below`,`:above`,`:missing` |exact match search| -|Token |`:missing`,`:not` |exact match search| +|Token |`:missing`,`:not`,`:of-type` |exact match search| |Number |`:missing` |implicit range search (see http://hl7.org/fhir/R4/search.html#number)| |Date |`:missing` |implicit range search (see https://www.hl7.org/fhir/search.html#date)| |Quantity |`:missing` |implicit range search (see http://hl7.org/fhir/R4/search.html#quantity)| @@ -174,7 +174,7 @@ FHIR search modifiers are described at https://www.hl7.org/fhir/R4/search.html#m Due to performance implications, the `:exact` modifier should be used for String searches where possible. -The `:text`, `:above`, `:below`, `:in`, `:not-in`, and `:of-type` modifiers are not supported in this version of the IBM FHIR server and use of these modifiers will result in an HTTP 400 error with an OperationOutcome that describes the failure. +The `:text`, `:above`, `:below`, `:in`, and `:not-in` modifiers are not supported in this version of the IBM FHIR server and use of these modifiers will result in an HTTP 400 error with an OperationOutcome that describes the failure. ### Search prefixes FHIR search prefixes are described at https://www.hl7.org/fhir/R4/search.html#prefix. diff --git a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/JDBCConstants.java b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/JDBCConstants.java index bd9e777f140..8ba3b37f4d6 100644 --- a/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/JDBCConstants.java +++ b/fhir-persistence-jdbc/src/main/java/com/ibm/fhir/persistence/jdbc/JDBCConstants.java @@ -134,7 +134,7 @@ public class JDBCConstants { supportedModifiersMap.put(Type.STRING, Arrays.asList(Modifier.EXACT, Modifier.CONTAINS, Modifier.MISSING)); supportedModifiersMap.put(Type.REFERENCE, Arrays.asList(Modifier.TYPE, Modifier.MISSING, Modifier.IDENTIFIER)); supportedModifiersMap.put(Type.URI, Arrays.asList(Modifier.BELOW, Modifier.ABOVE, Modifier.MISSING)); - supportedModifiersMap.put(Type.TOKEN, Arrays.asList(Modifier.MISSING, Modifier.NOT)); + supportedModifiersMap.put(Type.TOKEN, Arrays.asList(Modifier.MISSING, Modifier.NOT, Modifier.OF_TYPE)); supportedModifiersMap.put(Type.NUMBER, Arrays.asList(Modifier.MISSING)); supportedModifiersMap.put(Type.DATE, Arrays.asList(Modifier.MISSING)); supportedModifiersMap.put(Type.QUANTITY, Arrays.asList(Modifier.MISSING)); diff --git a/fhir-search/src/main/java/com/ibm/fhir/search/SearchConstants.java b/fhir-search/src/main/java/com/ibm/fhir/search/SearchConstants.java index 7461567f3d8..b4cbd7c6392 100644 --- a/fhir-search/src/main/java/com/ibm/fhir/search/SearchConstants.java +++ b/fhir-search/src/main/java/com/ibm/fhir/search/SearchConstants.java @@ -131,6 +131,8 @@ private SearchConstants() { public static final String PARAMETER_DELIMITER = "|"; + public static final String COMPOSITE_DELIMITER = "$"; + public static final char COLON_DELIMITER = ':'; public static final String COLON_DELIMITER_STR = ":"; diff --git a/fhir-search/src/main/java/com/ibm/fhir/search/parameters/QueryParameterValue.java b/fhir-search/src/main/java/com/ibm/fhir/search/parameters/QueryParameterValue.java index 7acead33ccf..dcd8bc2b81a 100644 --- a/fhir-search/src/main/java/com/ibm/fhir/search/parameters/QueryParameterValue.java +++ b/fhir-search/src/main/java/com/ibm/fhir/search/parameters/QueryParameterValue.java @@ -21,6 +21,7 @@ public class QueryParameterValue { private boolean hidden = false; + private boolean ofTypeModifier = false; private Prefix prefix = null; @@ -150,6 +151,22 @@ public void setHidden(boolean hidden) { this.hidden = hidden; } + /** + * Sets whether the value of an :of-type modifier. + * @return true or false + */ + public boolean isOfTypeModifier() { + return ofTypeModifier; + } + + /** + * Gets whether the value of an :of-type modifier. + * @param ofTypeModifier true if value of an :of-type modifier + */ + public void setOfTypeModifier(boolean ofTypeModifier) { + this.ofTypeModifier = ofTypeModifier; + } + /** * Serialize the ParameterValue to a query parameter string */ @@ -178,6 +195,7 @@ public String toString() { delim = outputBuilder(returnString, delim, valueString); delim = outputBuilder(returnString, delim, valueDate); + // token search with :of-type modifier is handled as a composite search if (component != null && !component.isEmpty()) { String componentDelim = ""; for (QueryParameter componentParam : component) { @@ -190,7 +208,7 @@ public String toString() { throw new IllegalStateException("Components of a composite search parameter may only have a single value"); } returnString.append(componentDelim).append(componentValues.get(0)); - componentDelim = "$"; + componentDelim = ofTypeModifier ? SearchConstants.PARAMETER_DELIMITER : SearchConstants.COMPOSITE_DELIMITER; } } return returnString.toString(); diff --git a/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchUtil.java b/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchUtil.java index 6a592e75c27..238e4452b29 100644 --- a/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchUtil.java +++ b/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchUtil.java @@ -882,9 +882,15 @@ public static FHIRSearchContext parseQueryParameters(Class resourceType, // Build list of processed query parameters List curParameterList = new ArrayList<>(); for (String paramValueString : params) { - QueryParameter parameter = new QueryParameter(type, parameterCode, modifier, modifierResourceTypeName); List queryParameterValues = - processQueryParameterValueString(resourceType, searchParameter, modifier, parameter.getModifierResourceTypeName(), paramValueString); + processQueryParameterValueString(resourceType, searchParameter, modifier, modifierResourceTypeName, paramValueString); + QueryParameter parameter; + // Internally treat search with :of-type modifier as composite search + if (Modifier.OF_TYPE.equals(modifier)) { + parameter = new QueryParameter(Type.COMPOSITE, parameterCode + SearchConstants.OF_TYPE_MODIFIER_SUFFIX, null, null); + } else { + parameter = new QueryParameter(type, parameterCode, modifier, modifierResourceTypeName); + } parameter.getValues().addAll(queryParameterValues); curParameterList.add(parameter); parameters.add(parameter); @@ -1238,12 +1244,10 @@ private static List parseQueryParameterValuesString(SearchP if (parts.length == 2) { parameterValue.setValueSystem(unescapeSearchParm(parts[0])); parameterValue.setValueCode(unescapeSearchParm(parts[1])); - } - else { + } else { parameterValue.setValueCode(unescapeSearchParm(v)); } - } - else { + } else { String valueString = unescapeSearchParm(v); valueString = extractReferenceValue(valueString); parameterValue.setValueString(valueString); @@ -1281,6 +1285,7 @@ private static List parseQueryParameterValuesString(SearchP case TOKEN: { // token // [parameter]=[system]|[code] + // [parameter]:of-type=[system|code|value] /* * TODO: start enforcing this: * "For token parameters on elements of type ContactPoint, uri, or boolean, @@ -1288,6 +1293,33 @@ private static List parseQueryParameterValuesString(SearchP * [parameter]=[code] form is allowed */ String[] parts = v.split(SearchConstants.BACKSLASH_NEGATIVE_LOOKBEHIND + "\\|"); + if (Modifier.OF_TYPE.equals(modifier)) { + // Convert :of-type into a composite search parameter + final String ofTypeParmName = searchParameter.getCode().getValue() + SearchConstants.OF_TYPE_MODIFIER_SUFFIX; + parameterValue.setOfTypeModifier(true); + if (parts.length > 1 && parts.length < 4) { + QueryParameterValue typeParameterValue = new QueryParameterValue(); + if (parts.length == 3) { + typeParameterValue.setValueSystem(unescapeSearchParm(parts[0])); + } + typeParameterValue.setValueCode(unescapeSearchParm(parts[parts.length - 2])); + QueryParameter typeParameter = new QueryParameter(Type.TOKEN, SearchUtil.makeCompositeSubCode(ofTypeParmName, SearchConstants.OF_TYPE_MODIFIER_COMPONENT_TYPE), + null, null, Collections.singletonList(typeParameterValue)); + parameterValue.addComponent(typeParameter); + + QueryParameterValue valueParameterValue = new QueryParameterValue(); + valueParameterValue.setValueCode(unescapeSearchParm(parts[parts.length - 1])); + QueryParameter valueParameter = new QueryParameter(Type.TOKEN, SearchUtil.makeCompositeSubCode(ofTypeParmName, SearchConstants.OF_TYPE_MODIFIER_COMPONENT_VALUE), + null, null, Collections.singletonList(valueParameterValue)); + parameterValue.addComponent(valueParameter); + } else { + QueryParameterValue valueParameterValue = new QueryParameterValue(); + valueParameterValue.setValueCode(unescapeSearchParm(v)); + QueryParameter valueParameter = new QueryParameter(Type.TOKEN, SearchUtil.makeCompositeSubCode(ofTypeParmName, SearchConstants.OF_TYPE_MODIFIER_COMPONENT_VALUE), + null, null, Collections.singletonList(valueParameterValue)); + parameterValue.addComponent(valueParameter); + } + } else if (parts.length == 2) { parameterValue.setValueSystem(unescapeSearchParm(parts[0])); parameterValue.setValueCode(unescapeSearchParm(parts[1])); diff --git a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SearchTest.java b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SearchTest.java index 49239c5ac17..706664b88e7 100644 --- a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SearchTest.java +++ b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SearchTest.java @@ -51,12 +51,14 @@ import com.ibm.fhir.model.test.TestUtil; import com.ibm.fhir.model.type.Canonical; import com.ibm.fhir.model.type.Code; +import com.ibm.fhir.model.type.CodeableConcept; import com.ibm.fhir.model.type.Coding; import com.ibm.fhir.model.type.Decimal; import com.ibm.fhir.model.type.Identifier; import com.ibm.fhir.model.type.Meta; import com.ibm.fhir.model.type.Quantity; import com.ibm.fhir.model.type.Reference; +import com.ibm.fhir.model.type.Uri; import com.ibm.fhir.model.type.code.AdministrativeGender; import com.ibm.fhir.model.type.code.ResourceType; import com.ibm.fhir.model.util.FHIRUtil; @@ -67,6 +69,7 @@ public class SearchTest extends FHIRServerTestBase { private static final String PREFER_HEADER_RETURN_REPRESENTATION = "return=representation"; private static final String PREFER_HEADER_NAME = "Prefer"; private String patientId; + private String patientIdentifierValue; private String observationId; private Boolean compartmentSearchSupported = null; private String practitionerId; @@ -147,12 +150,21 @@ public void testCreateOrganization() throws Exception { @Test(groups = { "server-search" }, dependsOnMethods = {"testCreateOrganization" }) public void testCreatePatient() throws Exception { WebTarget target = getWebTarget(); + patientIdentifierValue = UUID.randomUUID().toString(); // Build a new Patient and then call the 'create' API. Patient patient = TestUtil.readLocalResource("Patient_JohnDoe.json"); patient = patient.toBuilder() .gender(AdministrativeGender.MALE) + .identifier(Identifier.builder() + .value(string(patientIdentifierValue)) + .system(uri("test")) + .type(CodeableConcept.builder() + .coding(Coding.builder().code(Code.of("typecodea")).system(Uri.of("typesystema")).build()) + .coding(Coding.builder().code(Code.of("typecodeb")).build()) + .build()) + .build()) .managingOrganization(Reference.builder() .reference(com.ibm.fhir.model.type.String.of("Organization/" + organizationId + "/_history/1")) .build()) @@ -434,6 +446,111 @@ public void testSearchPatientWithGenderNot() { assertTrue(bundle.getEntry().size() >= 1); } + @Test(groups = { "server-search" }, dependsOnMethods = {"testCreatePatient" }) + public void testSearchPatientWithIdentifierValueOnly() { + WebTarget target = getWebTarget(); + Response response = + target.path("Patient").queryParam("identifier", patientIdentifierValue) + .request(FHIRMediaType.APPLICATION_FHIR_JSON) + .header("X-FHIR-TENANT-ID", tenantName) + .header("X-FHIR-DSID", dataStoreId) + .get(); + assertResponse(response, Response.Status.OK.getStatusCode()); + Bundle bundle = response.readEntity(Bundle.class); + assertNotNull(bundle); + assertTrue(bundle.getEntry().size() >= 1); + } + + @Test(groups = { "server-search" }, dependsOnMethods = {"testCreatePatient" }) + public void testSearchPatientWithIdentifier() { + WebTarget target = getWebTarget(); + Response response = + target.path("Patient").queryParam("identifier", "test|"+ patientIdentifierValue) + .request(FHIRMediaType.APPLICATION_FHIR_JSON) + .header("X-FHIR-TENANT-ID", tenantName) + .header("X-FHIR-DSID", dataStoreId) + .get(); + assertResponse(response, Response.Status.OK.getStatusCode()); + Bundle bundle = response.readEntity(Bundle.class); + assertNotNull(bundle); + assertTrue(bundle.getEntry().size() >= 1); + } + + @Test(groups = { "server-search" }, dependsOnMethods = {"testCreatePatient" }) + public void testSearchPatientWithIdentifierNotFound() { + WebTarget target = getWebTarget(); + Response response = + target.path("Patient").queryParam("identifier", "typesystema|"+ patientIdentifierValue) + .request(FHIRMediaType.APPLICATION_FHIR_JSON) + .header("X-FHIR-TENANT-ID", tenantName) + .header("X-FHIR-DSID", dataStoreId) + .get(); + assertResponse(response, Response.Status.OK.getStatusCode()); + Bundle bundle = response.readEntity(Bundle.class); + assertNotNull(bundle); + assertTrue(bundle.getEntry().isEmpty()); + } + + @Test(groups = { "server-search" }, dependsOnMethods = {"testCreatePatient" }) + public void testSearchPatientWithIdentifierOfTypeValueOnly() { + WebTarget target = getWebTarget(); + Response response = + target.path("Patient").queryParam("identifier:of-type", patientIdentifierValue) + .request(FHIRMediaType.APPLICATION_FHIR_JSON) + .header("X-FHIR-TENANT-ID", tenantName) + .header("X-FHIR-DSID", dataStoreId) + .get(); + assertResponse(response, Response.Status.OK.getStatusCode()); + Bundle bundle = response.readEntity(Bundle.class); + assertNotNull(bundle); + assertTrue(bundle.getEntry().size() >= 1); + } + + @Test(groups = { "server-search" }, dependsOnMethods = {"testCreatePatient" }) + public void testSearchPatientWithIdentifierOfTypeCodeAndValueOnly() { + WebTarget target = getWebTarget(); + Response response = + target.path("Patient").queryParam("identifier:of-type", "typecodeb|"+ patientIdentifierValue) + .request(FHIRMediaType.APPLICATION_FHIR_JSON) + .header("X-FHIR-TENANT-ID", tenantName) + .header("X-FHIR-DSID", dataStoreId) + .get(); + assertResponse(response, Response.Status.OK.getStatusCode()); + Bundle bundle = response.readEntity(Bundle.class); + assertNotNull(bundle); + assertTrue(bundle.getEntry().size() >= 1); + } + + @Test(groups = { "server-search" }, dependsOnMethods = {"testCreatePatient" }) + public void testSearchPatientWithIdentifierOfType() { + WebTarget target = getWebTarget(); + Response response = + target.path("Patient").queryParam("identifier:of-type", "typesystema|typecodea|"+ patientIdentifierValue) + .request(FHIRMediaType.APPLICATION_FHIR_JSON) + .header("X-FHIR-TENANT-ID", tenantName) + .header("X-FHIR-DSID", dataStoreId) + .get(); + assertResponse(response, Response.Status.OK.getStatusCode()); + Bundle bundle = response.readEntity(Bundle.class); + assertNotNull(bundle); + assertTrue(bundle.getEntry().size() >= 1); + } + + @Test(groups = { "server-search" }, dependsOnMethods = {"testCreatePatient" }) + public void testSearchPatientWithIdentifierOfTypeNotFound() { + WebTarget target = getWebTarget(); + Response response = + target.path("Patient").queryParam("identifier:of-type", "test|typecodeb|"+ patientIdentifierValue) + .request(FHIRMediaType.APPLICATION_FHIR_JSON) + .header("X-FHIR-TENANT-ID", tenantName) + .header("X-FHIR-DSID", dataStoreId) + .get(); + assertResponse(response, Response.Status.OK.getStatusCode()); + Bundle bundle = response.readEntity(Bundle.class); + assertNotNull(bundle); + assertTrue(bundle.getEntry().isEmpty()); + } + @Test(groups = { "server-search" }) public void testCreateObservationWithRange() throws Exception { WebTarget target = getWebTarget(); From 3ab101081c02ab42a7207e0aa3f303b157577abe Mon Sep 17 00:00:00 2001 From: Troy Biesterfeld Date: Mon, 22 Mar 2021 11:05:32 -0500 Subject: [PATCH 3/5] Issue #1323 - Update after review comments Signed-off-by: Troy Biesterfeld --- docs/src/pages/Conformance.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/pages/Conformance.md b/docs/src/pages/Conformance.md index ef67f307e76..53d530abaa7 100644 --- a/docs/src/pages/Conformance.md +++ b/docs/src/pages/Conformance.md @@ -2,7 +2,7 @@ layout: post title: Conformance description: Notes on the Conformance of the IBM FHIR Server -date: 2021-03-19 12:00:00 -0400 +date: 2021-03-22 12:00:00 -0400 permalink: /conformance/ --- @@ -172,9 +172,9 @@ FHIR search modifiers are described at https://www.hl7.org/fhir/R4/search.html#m |Composite |`:missing` |processes each parameter component according to its type| |Special (near) | none |searches a bounding area according to the value of the `fhirServer/search/useBoundingRadius` property| -Due to performance implications, the `:exact` modifier should be used for String searches where possible. +Due to performance implications, the `:exact` modifier should be used for String search parameters when possible. -The `:text`, `:above`, `:below`, `:in`, and `:not-in` modifiers are not supported in this version of the IBM FHIR server and use of these modifiers will result in an HTTP 400 error with an OperationOutcome that describes the failure. +The `:text` modifier, as well as the `:above`, `:below`, `:in`, and `:not-in` modifiers for Token search parameters, are not yet supported by the IBM FHIR server. Use of these modifiers will result in an HTTP 400 error with an OperationOutcome that describes the failure. ### Search prefixes FHIR search prefixes are described at https://www.hl7.org/fhir/R4/search.html#prefix. From 8dcb01b9d8ad5d364c3f1060ab7b92b0ee686eba Mon Sep 17 00:00:00 2001 From: Troy Biesterfeld Date: Mon, 22 Mar 2021 12:14:19 -0500 Subject: [PATCH 4/5] Issue #1323: Update after review comments Signed-off-by: Troy Biesterfeld --- .../parameters/QueryParameterValue.java | 10 ++++--- .../search/test/QueryParameterValueTest.java | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/fhir-search/src/main/java/com/ibm/fhir/search/parameters/QueryParameterValue.java b/fhir-search/src/main/java/com/ibm/fhir/search/parameters/QueryParameterValue.java index dcd8bc2b81a..73a778ec8d4 100644 --- a/fhir-search/src/main/java/com/ibm/fhir/search/parameters/QueryParameterValue.java +++ b/fhir-search/src/main/java/com/ibm/fhir/search/parameters/QueryParameterValue.java @@ -152,7 +152,7 @@ public void setHidden(boolean hidden) { } /** - * Sets whether the value of an :of-type modifier. + * Gets whether the value is of an :of-type modifier. * @return true or false */ public boolean isOfTypeModifier() { @@ -160,8 +160,8 @@ public boolean isOfTypeModifier() { } /** - * Gets whether the value of an :of-type modifier. - * @param ofTypeModifier true if value of an :of-type modifier + * Sets whether the value is of an :of-type modifier. + * @param ofTypeModifier true if value is of an :of-type modifier, otherwise false */ public void setOfTypeModifier(boolean ofTypeModifier) { this.ofTypeModifier = ofTypeModifier; @@ -195,7 +195,7 @@ public String toString() { delim = outputBuilder(returnString, delim, valueString); delim = outputBuilder(returnString, delim, valueDate); - // token search with :of-type modifier is handled as a composite search + // token search with :of-type modifier is handled internally as a composite search if (component != null && !component.isEmpty()) { String componentDelim = ""; for (QueryParameter componentParam : component) { @@ -208,6 +208,8 @@ public String toString() { throw new IllegalStateException("Components of a composite search parameter may only have a single value"); } returnString.append(componentDelim).append(componentValues.get(0)); + // Since the self/next/previous links generated by UriBuilder for the search response Bundle use this output, + // this needs to use "|" instead of "$" as the component delimiter componentDelim = ofTypeModifier ? SearchConstants.PARAMETER_DELIMITER : SearchConstants.COMPOSITE_DELIMITER; } } diff --git a/fhir-search/src/test/java/com/ibm/fhir/search/test/QueryParameterValueTest.java b/fhir-search/src/test/java/com/ibm/fhir/search/test/QueryParameterValueTest.java index 4820aa798a1..66c3f222f83 100644 --- a/fhir-search/src/test/java/com/ibm/fhir/search/test/QueryParameterValueTest.java +++ b/fhir-search/src/test/java/com/ibm/fhir/search/test/QueryParameterValueTest.java @@ -14,6 +14,8 @@ import org.testng.annotations.Test; import com.ibm.fhir.search.SearchConstants.Prefix; +import com.ibm.fhir.search.SearchConstants.Type; +import com.ibm.fhir.search.parameters.QueryParameter; import com.ibm.fhir.search.parameters.QueryParameterValue; /** @@ -86,4 +88,28 @@ public void testValueDate() throws Exception { assertEquals(testObj.toString(), "eq2019-12-11"); assertEquals(testObj.toString(), "eq2019-12-11"); } + + @Test + public void testToStringOfComposite() throws Exception { + QueryParameterValue testObj = new QueryParameterValue(); + QueryParameter qp1 = new QueryParameter(Type.TOKEN, "qp1", null, null); + QueryParameterValue qpv1 = new QueryParameterValue(); + String valueSystem1 = "valueSystem1"; + String valueCode1 = "valueCode1"; + qpv1.setValueSystem(valueSystem1); + qpv1.setValueCode(valueCode1); + qp1.getValues().add(qpv1); + QueryParameter qp2 = new QueryParameter(Type.TOKEN, "qp2", null, null); + QueryParameterValue qpv2 = new QueryParameterValue(); + String valueCode2 = "valueCode2"; + qpv2.setValueCode(valueCode2); + qp2.getValues().add(qpv2); + testObj.addComponent(qp1, qp2); + // toString of an :of-type modifier uses '|' as component delimiter + testObj.setOfTypeModifier(true); + assertEquals(testObj.toString(), "valueSystem1|valueCode1|valueCode2"); + // Otherwise toString uses '$' as component delimiter + testObj.setOfTypeModifier(false); + assertEquals(testObj.toString(), "valueSystem1|valueCode1$valueCode2"); + } } From 7dd99b68e67d0a197cf1ce2340c738a34f294444 Mon Sep 17 00:00:00 2001 From: Troy Biesterfeld Date: Mon, 22 Mar 2021 14:39:12 -0500 Subject: [PATCH 5/5] Issue #1323 - Update after review comments Signed-off-by: Troy Biesterfeld --- .../src/main/java/com/ibm/fhir/search/util/SearchUtil.java | 5 ++++- .../src/test/java/com/ibm/fhir/server/test/SearchTest.java | 7 +++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchUtil.java b/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchUtil.java index 238e4452b29..4526d0d7e3f 100644 --- a/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchUtil.java +++ b/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchUtil.java @@ -1297,7 +1297,10 @@ private static List parseQueryParameterValuesString(SearchP // Convert :of-type into a composite search parameter final String ofTypeParmName = searchParameter.getCode().getValue() + SearchConstants.OF_TYPE_MODIFIER_SUFFIX; parameterValue.setOfTypeModifier(true); - if (parts.length > 1 && parts.length < 4) { + if (parts.length < 2) { + String msg = "Search parameter '" + searchParameter.getCode().getValue() + "' with modifier ':" + modifier.value() + "' requires at least a code and value"; + throw SearchExceptionUtil.buildNewInvalidSearchException(msg); + } else if (parts.length < 4) { QueryParameterValue typeParameterValue = new QueryParameterValue(); if (parts.length == 3) { typeParameterValue.setValueSystem(unescapeSearchParm(parts[0])); diff --git a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SearchTest.java b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SearchTest.java index 706664b88e7..f39ae812761 100644 --- a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SearchTest.java +++ b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SearchTest.java @@ -500,10 +500,9 @@ public void testSearchPatientWithIdentifierOfTypeValueOnly() { .header("X-FHIR-TENANT-ID", tenantName) .header("X-FHIR-DSID", dataStoreId) .get(); - assertResponse(response, Response.Status.OK.getStatusCode()); - Bundle bundle = response.readEntity(Bundle.class); - assertNotNull(bundle); - assertTrue(bundle.getEntry().size() >= 1); + assertResponse(response, Response.Status.BAD_REQUEST.getStatusCode()); + assertExceptionOperationOutcome(response.readEntity(OperationOutcome.class), + "Search parameter 'identifier' with modifier ':of-type' requires at least a code and value"); } @Test(groups = { "server-search" }, dependsOnMethods = {"testCreatePatient" })