diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BaseIdentifiableElement.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BaseIdentifiableElement.java index 63065591760..2f2c713061b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BaseIdentifiableElement.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BaseIdentifiableElement.java @@ -22,6 +22,7 @@ package ca.uhn.fhir.model.api; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.parser.DataFormatException; +import ca.uhn.fhir.util.CoverageIgnore; public abstract class BaseIdentifiableElement extends BaseElement implements IIdentifiableElement { @@ -36,6 +37,7 @@ public abstract class BaseIdentifiableElement extends BaseElement implements IId * @deprecated Use {@link #getElementSpecificId()} instead. This method will be removed because it is easily * confused with other ID methods (such as patient#getIdentifier) */ + @CoverageIgnore @Deprecated @Override public IdDt getId() { @@ -55,6 +57,7 @@ public abstract class BaseIdentifiableElement extends BaseElement implements IId * @deprecated Use {@link #setElementSpecificId(String)} instead. This method will be removed because it is easily * confused with other ID methods (such as patient#getIdentifier) */ + @CoverageIgnore @Deprecated @Override public void setId(IdDt theId) { @@ -69,27 +72,33 @@ public abstract class BaseIdentifiableElement extends BaseElement implements IId * @deprecated Use {@link #setElementSpecificId(String)} instead. This method will be removed because it is easily * confused with other ID methods (such as patient#getIdentifier) */ + @CoverageIgnore @Override @Deprecated public void setId(String theId) { myElementSpecificId = theId; } + @CoverageIgnore private static class LockedId extends IdDt { + @CoverageIgnore public LockedId() { } + @CoverageIgnore public LockedId(String theElementSpecificId) { super(theElementSpecificId); } @Override + @CoverageIgnore public IdDt setValue(String theValue) throws DataFormatException { throw new UnsupportedOperationException("Use IElement#setElementSpecificId(String) to set the element ID for an element"); } @Override + @CoverageIgnore public void setValueAsString(String theValue) throws DataFormatException { throw new UnsupportedOperationException("Use IElement#setElementSpecificId(String) to set the element ID for an element"); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/ValidationModeEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/ValidationModeEnum.java index 3a856e32d6f..44c8edfcc15 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/ValidationModeEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/ValidationModeEnum.java @@ -1,5 +1,9 @@ package ca.uhn.fhir.rest.api; +import java.util.HashMap; + +import org.apache.commons.lang3.Validate; + /* * #%L * HAPI FHIR - Core Library @@ -27,18 +31,41 @@ public enum ValidationModeEnum { /** * The server checks the content, and then checks that the content would be acceptable as a create (e.g. that the content would not validate any uniqueness constraints) */ - CREATE, + CREATE("create"), /** * The server checks the content, and then checks that it would accept it as an update against the nominated specific resource (e.g. that there are no changes to immutable fields the server does not allow to change, and checking version integrity if appropriate) */ - UPDATE, + UPDATE("update"), /** * The server ignores the content, and checks that the nominated resource is allowed to be deleted (e.g. checking referential integrity rules) */ - DELETE; + DELETE("delete"); + private static HashMap myCodeToValue; + private String myCode; + + static { + myCodeToValue = new HashMap(); + for (ValidationModeEnum next : values()) { + myCodeToValue.put(next.getCode(), next); + } + } + + public static ValidationModeEnum forCode(String theCode) { + Validate.notBlank(theCode, "theCode must not be blank"); + return myCodeToValue.get(theCode); + } + + public String getCode() { + return myCode; + } + + private ValidationModeEnum(String theCode) { + myCode = theCode; + } + // @Override // public boolean isEmpty() { // return false; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java index 093e453ef7f..b55cbafa022 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java @@ -498,12 +498,20 @@ public class MethodUtil { param = new OperationParameter(theContext, Constants.EXTOP_VALIDATE, Constants.EXTOP_VALIDATE_MODE, 0, 1).setConverter(new IConverter() { @Override public Object incomingServer(Object theObject) { - return ValidationModeEnum.valueOf(theObject.toString().toUpperCase()); + if (isNotBlank(theObject.toString())) { + ValidationModeEnum retVal = ValidationModeEnum.forCode(theObject.toString()); + if (retVal == null) { + OperationParameter.throwInvalidMode(theObject.toString()); + } + return retVal; + } else { + return null; + } } @Override public Object outgoingClient(Object theObject) { - return new StringDt(((ValidationModeEnum)theObject).name().toLowerCase()); + return new StringDt(((ValidationModeEnum)theObject).getCode()); } }); } else if (nextAnnotation instanceof Validate.Profile) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java index 501a4231717..c21b48d7ea4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/OperationParameter.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.rest.method; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + /* * #%L * HAPI FHIR - Core Library @@ -179,7 +181,7 @@ public class OperationParameter implements IParameter { */ isSearchParam &= typeIsConcrete && !IBase.class.isAssignableFrom(myParameterType); - myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) || String.class.equals(myParameterType) || isSearchParam; + myAllowGet = IPrimitiveType.class.isAssignableFrom(myParameterType) || String.class.equals(myParameterType) || isSearchParam || ValidationModeEnum.class.equals(myParameterType); /* * The parameter can be of type string for validation methods - This is a bit weird. See ValidateDstu2Test. We @@ -291,7 +293,17 @@ public class OperationParameter implements IParameter { for (String next : paramValues) { matchingParamValues.add(next); } - + } else if (ValidationModeEnum.class.equals(myParameterType)) { + + if (isNotBlank(paramValues[0])) { + ValidationModeEnum validationMode = ValidationModeEnum.forCode(paramValues[0]); + if (validationMode != null) { + matchingParamValues.add(validationMode); + } else { + throwInvalidMode(paramValues[0]); + } + } + } else { for (String nextValue : paramValues) { FhirContext ctx = theRequest.getServer().getFhirContext(); @@ -374,6 +386,10 @@ public class OperationParameter implements IParameter { } } + public static void throwInvalidMode(String paramValues) { + throw new InvalidRequestException("Invalid mode value: \"" + paramValues + "\""); + } + @SuppressWarnings("unchecked") private void tryToAddValues(List theParamValues, List theMatchingParamValues) { for (Object nextValue : theParamValues) { @@ -436,4 +452,5 @@ public class OperationParameter implements IParameter { } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/BundleProviders.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/BundleProviders.java index 97e2c176ee4..84668a7ab9f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/BundleProviders.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/BundleProviders.java @@ -26,6 +26,7 @@ import java.util.List; import org.hl7.fhir.instance.model.api.IBaseResource; import ca.uhn.fhir.model.primitive.InstantDt; +import ca.uhn.fhir.util.CoverageIgnore; /** * Utility methods for working with {@link IBundleProvider} @@ -33,6 +34,7 @@ import ca.uhn.fhir.model.primitive.InstantDt; public class BundleProviders { /** Non instantiable */ + @CoverageIgnore private BundleProviders() { //nothing } diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java b/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java index f758c163b1b..9ecd47abcfa 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java +++ b/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/UploadTerminologyCommand.java @@ -45,6 +45,7 @@ import org.hl7.fhir.dstu3.model.IdType; import org.hl7.fhir.dstu3.model.Parameters; import org.hl7.fhir.dstu3.model.Resource; import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.dstu3.model.UriType; import org.hl7.fhir.instance.model.api.IBase; import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseParameters; @@ -138,7 +139,7 @@ public class UploadTerminologyCommand extends BaseCommand { IBaseParameters inputParameters; if (ctx.getVersion().getVersion() == FhirVersionEnum.DSTU3) { Parameters p = new Parameters(); - p.addParameter().setName("url").setValue(new StringType(termUrl)); + p.addParameter().setName("url").setValue(new UriType(termUrl)); p.addParameter().setName("localfile").setValue(new StringType(datafile)); inputParameters = p; } else { @@ -154,6 +155,7 @@ public class UploadTerminologyCommand extends BaseCommand { .execute(); ourLog.info("Upload complete!"); + ourLog.info("Response:\n{}", ctx.newXmlParser().setPrettyPrint(true).encodeResourceToString(response)); } } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java index f6d10836cd7..0d4f3fc77fe 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java @@ -1428,16 +1428,6 @@ public class SearchBuilder { return singleCode; } - private Set toCodes(Set theCodeConcepts) { - HashSet retVal = Sets.newHashSet(); - for (TermConcept next : theCodeConcepts) { - if (isNotBlank(next.getCode())) { - retVal.add(next.getCode()); - } - } - return retVal; - } - private Predicate createResourceLinkPathPredicate(String theParamName, Root from) { return createResourceLinkPathPredicate(myContext, theParamName, from, myResourceType); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3.java index 1e7c31a3a3a..c808065674a 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3.java @@ -131,7 +131,9 @@ public class FhirResourceDaoDstu3 extends BaseHapiFhirRe validator.registerValidatorModule(new IdChecker(theMode)); ValidationResult result; - if (isNotBlank(theRawResource)) { + if (theResource == null) { + throw new InvalidRequestException("No resource supplied for $validate operation (resource is required unless mode is \"delete\")"); + } else if (isNotBlank(theRawResource)) { result = validator.validateWithResult(theRawResource); } else { result = validator.validateWithResult(theResource); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvc.java index 215420a686d..65ea06b0dd9 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/BaseHapiTerminologySvc.java @@ -217,12 +217,13 @@ public abstract class BaseHapiTerminologySvc implements IHapiTerminologySvc { } } - ourLog.info("Validating code system"); + ourLog.info("Validating all codes in CodeSystem for storage (this can take some time for large sets)"); // Validate the code system IdentityHashMap conceptsStack = new IdentityHashMap(); + int totalCodeCount = 0; for (TermConcept next : theCodeSystem.getConcepts()) { - validateConceptForStorage(next, theCodeSystem, conceptsStack); + totalCodeCount += validateConceptForStorage(next, theCodeSystem, conceptsStack); } ourLog.info("Saving version"); @@ -234,7 +235,7 @@ public abstract class BaseHapiTerminologySvc implements IHapiTerminologySvc { codeSystem.setCurrentVersion(theCodeSystem); myCodeSystemDao.save(codeSystem); - ourLog.info("Saving concepts..."); + ourLog.info("Saving {} concepts...", totalCodeCount); conceptsStack = new IdentityHashMap(); for (TermConcept next : theCodeSystem.getConcepts()) { @@ -275,20 +276,24 @@ public abstract class BaseHapiTerminologySvc implements IHapiTerminologySvc { return retVal; } - private void validateConceptForStorage(TermConcept theConcept, TermCodeSystemVersion theCodeSystem, IdentityHashMap theConceptsStack) { + private int validateConceptForStorage(TermConcept theConcept, TermCodeSystemVersion theCodeSystem, IdentityHashMap theConceptsStack) { ValidateUtil.isNotNullOrThrowInvalidRequest(theConcept.getCodeSystem() == theCodeSystem, "Codesystem contains a code which does not reference the codesystem"); ValidateUtil.isNotBlankOrThrowInvalidRequest(theConcept.getCode(), "Codesystem contains a code which does not reference the codesystem"); + if (theConceptsStack.put(theConcept, PLACEHOLDER_OBJECT) != null) { throw new InvalidRequestException("CodeSystem contains circular reference around code " + theConcept.getCode()); } + int retVal = 1; for (TermConceptParentChildLink next : theConcept.getChildren()) { next.setCodeSystem(theCodeSystem); - validateConceptForStorage(next.getChild(), theCodeSystem, theConceptsStack); + retVal += validateConceptForStorage(next.getChild(), theCodeSystem, theConceptsStack); } theConceptsStack.remove(theConcept); + + return retVal; } public TermConcept findCode(String theCodeSystem, String theCode) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvc.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvc.java index 4af130d3516..31ac8a0692e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvc.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/TerminologyLoaderSvc.java @@ -34,6 +34,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -46,6 +47,7 @@ import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVRecord; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import com.google.common.annotations.VisibleForTesting; @@ -68,20 +70,35 @@ public class TerminologyLoaderSvc implements IHapiTerminologyLoaderSvc { @Autowired private IHapiTerminologySvc myTermSvc; - private void dropCircularRefs(TermConcept theConcept, HashSet theChain) { + private void dropCircularRefs(TermConcept theConcept, LinkedHashSet theChain, Map theCode2concept) { + theChain.add(theConcept.getCode()); for (Iterator childIter = theConcept.getChildren().iterator(); childIter.hasNext();) { TermConceptParentChildLink next = childIter.next(); TermConcept nextChild = next.getChild(); if (theChain.contains(nextChild.getCode())) { - ourLog.info("Removing circular reference code {} from parent {}", nextChild.getCode(), theConcept.getCode()); + + StringBuilder b = new StringBuilder(); + b.append("Removing circular reference code "); + b.append(nextChild.getCode()); + b.append(" from parent "); + b.append(nextChild.getCode()); + b.append(". Chain was: "); + for (String nextInChain : theChain) { + TermConcept nextCode = theCode2concept.get(nextInChain); + b.append(nextCode.getCode()); + b.append('['); + b.append(StringUtils.substring(nextCode.getDisplay(), 0, 20).replace("[", "").replace("]", "").trim()); + b.append("] "); + } + ourLog.info(b.toString(), theConcept.getCode()); childIter.remove(); } else { - theChain.add(theConcept.getCode()); - dropCircularRefs(nextChild, theChain); - theChain.remove(theConcept.getCode()); + dropCircularRefs(nextChild, theChain, theCode2concept); } } + theChain.remove(theConcept.getCode()); + } private TermConcept getOrCreateConcept(TermCodeSystemVersion codeSystemVersion, Map id2concept, String id) { @@ -152,7 +169,7 @@ public class TerminologyLoaderSvc implements IHapiTerminologyLoaderSvc { continue; } - ourLog.debug("Streaming ZIP entry {} into temporary file", nextEntry.getName()); + ourLog.info("Streaming ZIP entry {} into temporary file", nextEntry.getName()); File nextOutFile = File.createTempFile("hapi_fhir", ".csv"); nextOutFile.deleteOnExit(); @@ -206,7 +223,7 @@ public class TerminologyLoaderSvc implements IHapiTerminologyLoaderSvc { ourLog.info("Done loading SNOMED CT files - {} root codes, {} total codes", rootConcepts.size(), code2concept.size()); for (TermConcept next : rootConcepts.values()) { - dropCircularRefs(next, new HashSet()); + dropCircularRefs(next, new LinkedHashSet(), code2concept); } codeSystemVersion.getConcepts().addAll(rootConcepts.values()); diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java index 4a7a9c9679c..c6e8d00e318 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java @@ -51,6 +51,7 @@ import org.hl7.fhir.dstu3.model.Bundle.BundleType; import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb; import org.hl7.fhir.dstu3.model.Bundle.SearchEntryMode; import org.hl7.fhir.dstu3.model.CodeSystem; +import org.hl7.fhir.dstu3.model.CodeType; import org.hl7.fhir.dstu3.model.Coding; import org.hl7.fhir.dstu3.model.Condition; import org.hl7.fhir.dstu3.model.DateTimeType; @@ -2693,6 +2694,46 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { } } + @Test + public void testValidateBadInputViaPost() throws IOException { + + Parameters input = new Parameters(); + input.addParameter().setName("mode").setValue(new CodeType("create")); + + String inputStr = myFhirCtx.newXmlParser().encodeResourceToString(input); + ourLog.info(inputStr); + + HttpPost post = new HttpPost(ourServerBase + "/Patient/$validate"); + post.setEntity(new StringEntity(inputStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + + CloseableHttpResponse response = ourHttpClient.execute(post); + try { + String resp = IOUtils.toString(response.getEntity().getContent()); + ourLog.info(resp); + assertThat(resp, containsString("No resource supplied for $validate operation (resource is required unless mode is "delete")")); + assertEquals(400, response.getStatusLine().getStatusCode()); + } finally { + IOUtils.closeQuietly(response.getEntity().getContent()); + response.close(); + } + } + + @Test + public void testValidateBadInputViaGet() throws IOException { + + HttpGet get = new HttpGet(ourServerBase + "/Patient/$validate?mode=create"); + CloseableHttpResponse response = ourHttpClient.execute(get); + try { + String resp = IOUtils.toString(response.getEntity().getContent()); + ourLog.info(resp); + assertThat(resp, containsString("No resource supplied for $validate operation (resource is required unless mode is "delete")")); + assertEquals(400, response.getStatusLine().getStatusCode()); + } finally { + IOUtils.closeQuietly(response.getEntity().getContent()); + response.close(); + } + } + @Test public void testValidateResourceWithId() throws IOException { diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java index cc261a1af02..2e6ee88b8c7 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu2Test.java @@ -150,6 +150,24 @@ public class OperationServerDstu2Test { assertThat(response, containsString("HTTP Method GET is not allowed")); } + @Test + public void testOperationWrongParameterType() throws Exception { + Parameters p = new Parameters(); + p.addParameter().setName("PARAM1").setValue(new IntegerDt(123)); + String inParamsStr = ourCtx.newXmlParser().encodeResourceToString(p); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/123/$OP_INSTANCE"); + httpPost.setEntity(new StringEntity(inParamsStr, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + CloseableHttpResponse status = ourClient.execute(httpPost); + try { + String response = IOUtils.toString(status.getEntity().getContent()); + assertThat(response, containsString("Request has parameter PARAM1 of type IntegerDt but method expects type StringDt")); + ourLog.info(response); + } finally { + IOUtils.closeQuietly(status); + } + } + @Test public void testOperationOnInstance() throws Exception { Parameters p = new Parameters(); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchDstu2Test.java index d1f9a2734d7..7cbf365e373 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/SearchDstu2Test.java @@ -200,6 +200,18 @@ public class SearchDstu2Test { assertThat(responseContent, containsString("SYSTEM")); } + @Test + public void testSearchMethodReturnsNull() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=searchReturnNull"); + + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info(responseContent); + assertEquals("searchReturnNull", ourLastMethod); + assertThat(responseContent, containsString("")); + } + @Test public void testSearchByPostWithBodyAndUrlParams() throws Exception { HttpPost httpGet = new HttpPost("http://localhost:" + ourPort + "/Patient/_search?_format=json"); @@ -513,6 +525,14 @@ public class SearchDstu2Test { } //@formatter:on + //@formatter:off + @Search(queryName="searchReturnNull") + public List searchNoList() { + ourLastMethod = "searchReturnNull"; + return null; + } + //@formatter:on + //@formatter:off @Search() public List searchQuantity( diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu3Test.java new file mode 100644 index 00000000000..34b1ea17910 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/OperationServerDstu3Test.java @@ -0,0 +1,721 @@ +package ca.uhn.fhir.rest.server; + +import static org.hamcrest.Matchers.blankOrNullString; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +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.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.Conformance; +import org.hl7.fhir.dstu3.model.Conformance.ConformanceRestOperationComponent; +import org.hl7.fhir.dstu3.model.DateTimeType; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.IntegerType; +import org.hl7.fhir.dstu3.model.Money; +import org.hl7.fhir.dstu3.model.OperationDefinition; +import org.hl7.fhir.dstu3.model.Parameters; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.dstu3.model.UnsignedIntType; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.client.IGenericClient; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.TestUtil; + +public class OperationServerDstu3Test { + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx; + + private static IdType ourLastId; + private static String ourLastMethod; + private static StringType ourLastParam1; + private static Patient ourLastParam2; + private static List ourLastParam3; + private static Money ourLastParamMoney1; + private static UnsignedIntType ourLastParamUnsignedInt1; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(OperationServerDstu3Test.class); + private static int ourPort; + private static Server ourServer; + + @Before + public void before() { + ourLastParam1 = null; + ourLastParam2 = null; + ourLastParam3 = null; + ourLastParamUnsignedInt1 = null; + ourLastParamMoney1 = null; + ourLastId = null; + ourLastMethod = ""; + } + + + @Test + public void testConformance() throws Exception { + IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort); + Conformance p = client.fetchConformance().ofType(Conformance.class).execute(); + List ops = p.getRest().get(0).getOperation(); + assertThat(ops.size(), greaterThan(1)); + assertThat(ops.get(0).getDefinition().getReferenceElement().getValue(), startsWith("#")); + } + + @Test + public void testInstanceEverythingGet() throws Exception { + + // Try with a GET + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/123/$everything"); + CloseableHttpResponse status = ourClient.execute(httpGet); + + assertEquals(200, status.getStatusLine().getStatusCode()); + String response = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals("instance $everything", ourLastMethod); + assertThat(response, startsWith(" getResourceType() { + return Patient.class; + } + + //@formatter:off + @Operation(name="$OP_INSTANCE") + public Parameters opInstance( + @IdParam IdType theId, + @OperationParam(name="PARAM1") StringType theParam1, + @OperationParam(name="PARAM2") Patient theParam2 + ) { + //@formatter:on + + ourLastMethod = "$OP_INSTANCE"; + ourLastId = theId; + ourLastParam1 = theParam1; + ourLastParam2 = theParam2; + + Parameters retVal = new Parameters(); + retVal.addParameter().setName("RET1").setValue(new StringType("RETVAL1")); + return retVal; + } + + //@formatter:off + @Operation(name="$OP_INSTANCE_OR_TYPE") + public Parameters opInstanceOrType( + @IdParam(optional=true) IdType theId, + @OperationParam(name="PARAM1") StringType theParam1, + @OperationParam(name="PARAM2") Patient theParam2 + ) { + //@formatter:on + + ourLastMethod = "$OP_INSTANCE_OR_TYPE"; + ourLastId = theId; + ourLastParam1 = theParam1; + ourLastParam2 = theParam2; + + Parameters retVal = new Parameters(); + retVal.addParameter().setName("RET1").setValue(new StringType("RETVAL1")); + return retVal; + } + + //@formatter:off + @Operation(name="$OP_PROFILE_DT2", idempotent=true) + public Bundle opProfileType( + @OperationParam(name="PARAM1") Money theParam1 + ) { + //@formatter:on + + ourLastMethod = "$OP_PROFILE_DT2"; + ourLastParamMoney1 = theParam1; + + Bundle retVal = new Bundle(); + retVal.addEntry().getResponse().setStatus("100"); + return retVal; + } + + //@formatter:off + @Operation(name="$OP_PROFILE_DT", idempotent=true) + public Bundle opProfileType( + @OperationParam(name="PARAM1") UnsignedIntType theParam1 + ) { + //@formatter:on + + ourLastMethod = "$OP_PROFILE_DT"; + ourLastParamUnsignedInt1 = theParam1; + + Bundle retVal = new Bundle(); + retVal.addEntry().getResponse().setStatus("100"); + return retVal; + } + + //@formatter:off + @Operation(name="$OP_TYPE", idempotent=true) + public Parameters opType( + @OperationParam(name="PARAM1") StringType theParam1, + @OperationParam(name="PARAM2") Patient theParam2 + ) { + //@formatter:on + + ourLastMethod = "$OP_TYPE"; + ourLastParam1 = theParam1; + ourLastParam2 = theParam2; + + Parameters retVal = new Parameters(); + retVal.addParameter().setName("RET1").setValue(new StringType("RETVAL1")); + return retVal; + } + + //@formatter:off + @Operation(name="$OP_TYPE_ONLY_STRING", idempotent=true) + public Parameters opTypeOnlyString( + @OperationParam(name="PARAM1") StringType theParam1 + ) { + //@formatter:on + + ourLastMethod = "$OP_TYPE"; + ourLastParam1 = theParam1; + + Parameters retVal = new Parameters(); + retVal.addParameter().setName("RET1").setValue(new StringType("RETVAL1")); + return retVal; + } + + //@formatter:off + @Operation(name="$OP_TYPE_RET_BUNDLE") + public Bundle opTypeRetBundle( + @OperationParam(name="PARAM1") StringType theParam1, + @OperationParam(name="PARAM2") Patient theParam2 + ) { + //@formatter:on + + ourLastMethod = "$OP_TYPE_RET_BUNDLE"; + ourLastParam1 = theParam1; + ourLastParam2 = theParam2; + + Bundle retVal = new Bundle(); + retVal.addEntry().getResponse().setStatus("100"); + return retVal; + } + + @Operation(name = "$everything", idempotent=true) + public Bundle patientEverything(@IdParam IdType thePatientId) { + ourLastMethod = "instance $everything"; + ourLastId = thePatientId; + return new Bundle(); + } + + /** + * Just to make sure this method doesn't "steal" calls + */ + @Read + public Patient read(@IdParam IdType theId) { + ourLastMethod = "read"; + Patient retVal = new Patient(); + retVal.setId(theId); + return retVal; + } + + } + + public static class PlainProvider { + + //@formatter:off + @Operation(name="$OP_INSTANCE_BUNDLE_PROVIDER", idempotent=true) + public IBundleProvider opInstanceReturnsBundleProvider() { + ourLastMethod = "$OP_INSTANCE_BUNDLE_PROVIDER"; + + List resources = new ArrayList(); + for (int i =0; i < 100;i++) { + Patient p = new Patient(); + p.setId("Patient/" + i); + p.addName().addFamily("Patient " + i); + resources.add(p); + } + + return new SimpleBundleProvider(resources); + } + + //@formatter:off + @Operation(name="$OP_SERVER") + public Parameters opServer( + @OperationParam(name="PARAM1") StringType theParam1, + @OperationParam(name="PARAM2") Patient theParam2 + ) { + //@formatter:on + + ourLastMethod = "$OP_SERVER"; + ourLastParam1 = theParam1; + ourLastParam2 = theParam2; + + Parameters retVal = new Parameters(); + retVal.addParameter().setName("RET1").setValue(new StringType("RETVAL1")); + return retVal; + } + + //@formatter:off + @Operation(name="$OP_SERVER_LIST_PARAM") + public Parameters opServerListParam( + @OperationParam(name="PARAM2") Patient theParam2, + @OperationParam(name="PARAM3") List theParam3 + ) { + //@formatter:on + + ourLastMethod = "$OP_SERVER_LIST_PARAM"; + ourLastParam2 = theParam2; + ourLastParam3 = theParam3; + + Parameters retVal = new Parameters(); + retVal.addParameter().setName("RET1").setValue(new StringType("RETVAL1")); + return retVal; + } + + } + +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ValidateDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ValidateDstu3Test.java new file mode 100644 index 00000000000..c03b73bb4aa --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ValidateDstu3Test.java @@ -0,0 +1,298 @@ +package ca.uhn.fhir.rest.server; + +import static org.hamcrest.Matchers.stringContainsInOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.dstu3.model.CodeType; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.OperationOutcome; +import org.hl7.fhir.dstu3.model.Organization; +import org.hl7.fhir.dstu3.model.Parameters; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.StringType; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.annotation.Validate; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.ValidationModeEnum; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.TestUtil; + +public class ValidateDstu3Test { + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx = FhirContext.forDstu3(); + private static EncodingEnum ourLastEncoding; + private static ValidationModeEnum ourLastMode; + private static String ourLastProfile; + private static String ourLastResourceBody; + private static OperationOutcome ourOutcomeToReturn; + private static int ourPort; + private static Server ourServer; + public static Patient ourLastPatient; + + @Before() + public void before() { + ourLastResourceBody = null; + ourLastEncoding = null; + ourOutcomeToReturn = null; + ourLastMode = null; + ourLastProfile = null; + } + + + @Test + public void testValidate() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier().setValue("001"); + patient.addIdentifier().setValue("002"); + + Parameters params = new Parameters(); + params.addParameter().setName("resource").setResource(patient); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/$validate"); + httpPost.setEntity(new StringEntity(ourCtx.newXmlParser().encodeResourceToString(params), ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + + HttpResponse status = ourClient.execute(httpPost); + String resp = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(200, status.getStatusLine().getStatusCode()); + + assertThat(resp, stringContainsInOrder(" getResourceType() { + return Organization.class; + } + + @Validate() + public MethodOutcome validate(@ResourceParam String theResourceBody, @ResourceParam EncodingEnum theEncoding) { + ourLastResourceBody = theResourceBody; + ourLastEncoding = theEncoding; + + return new MethodOutcome(new IdType("001")); + } + + } + + public static class PatientProvider implements IResourceProvider { + + @Override + public Class getResourceType() { + return Patient.class; + } + + @Validate() + public MethodOutcome validatePatient(@ResourceParam Patient thePatient, @Validate.Mode ValidationModeEnum theMode, @Validate.Profile String theProfile) { + + ourLastPatient = thePatient; + IdType id; + if (thePatient != null) { + id = new IdType(thePatient.getIdentifier().get(0).getValue()); + if (thePatient.getIdElement().isEmpty() == false) { + id = thePatient.getIdElement(); + } + } else { + id = new IdType("1"); + } + ourLastMode = theMode; + ourLastProfile = theProfile; + + MethodOutcome outcome = new MethodOutcome(id.withVersion("002")); + outcome.setOperationOutcome(ourOutcomeToReturn); + return outcome; + } + + } + +} diff --git a/src/changes/changes.xml b/src/changes/changes.xml index f3e3cd56aff..acd64b5fed4 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -244,6 +244,10 @@ DateTime parser incorrectly parsed times where more than 3 digits of precision were provided on the seconds after the decimal point + + Improve error messages when the $validate operation is called but no resource + is actually supplied to validate +