From d15dbd4317a2c3422d94358a2b46679791a8d3a5 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Fri, 18 Jul 2014 17:49:14 -0400 Subject: [PATCH] Add interceptor framework --- hapi-fhir-base/.classpath | 1 + .../org.eclipse.wst.common.component | 1 + hapi-fhir-base/pom.xml | 2 +- hapi-fhir-base/src/changes/changes.xml | 13 + .../ca/uhn/fhir/model/api/BaseResource.java | 2 +- .../java/ca/uhn/fhir/parser/JsonParser.java | 5 +- .../java/ca/uhn/fhir/parser/XmlParser.java | 2 +- .../rest/client/ClientInvocationHandler.java | 20 ++ .../uhn/fhir/rest/client/GenericClient.java | 311 +++++++++++------- .../rest/client/HttpBasicAuthInterceptor.java | 34 +- .../fhir/rest/client/IClientInterceptor.java | 10 +- .../uhn/fhir/rest/client/IGenericClient.java | 17 + .../fhir/rest/client/api/IRestfulClient.java | 13 +- .../interceptor/BasicAuthInterceptor.java | 73 ++++ .../interceptor/CapturingInterceptor.java | 43 +++ .../interceptor/LoggingInterceptor.java | 184 +++++++++++ .../ca/uhn/fhir/rest/gclient/ICreate.java | 31 ++ .../uhn/fhir/rest/gclient/ICreateTyped.java | 40 +++ .../BaseHttpClientInvocationWithContents.java | 20 +- .../fhir/rest/method/CreateMethodBinding.java | 35 +- .../rest/method/HttpPostClientInvocation.java | 4 + .../ca/uhn/fhir/rest/server/Constants.java | 6 + .../uhn/fhir/rest/server/RestfulServer.java | 26 +- .../example/java/example/ClientExamples.java | 44 ++- .../src/site/xdoc/doc_rest_client.xml | 55 +++- .../ca/uhn/fhir/rest/client/ClientTest.java | 176 +++++----- .../fhir/rest/client/GenericClientTest.java | 49 ++- .../uhn/fhir/rest/client/InterceptorTest.java | 83 +++++ .../ca/uhn/fhir/rest/server/CreateTest.java | 267 +++++++++++++++ .../ca/uhn/fhir/rest/server/PagingTest.java | 50 +-- .../rest/server/ResfulServerMethodTest.java | 89 ----- .../fhir/rest/server/ServerFeaturesTest.java | 37 +++ .../java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java | 5 + .../ca/uhn/fhir/jpa/dao/FhirResourceDao.java | 44 ++- .../uhn/fhir/jpa/entity/BaseHasResource.java | 3 + .../test/CompleteResourceProviderTest.java | 111 ++++--- .../org.eclipse.wst.common.component | 8 +- hapi-fhir-jpaserver-uhnfhirtest/pom.xml | 7 +- .../ca/uhn/fhirtest/TestRestfulServer.java | 2 +- .../ca/uhn/fhirtest/PopulateProfiles.java | 26 ++ .../main/java/ca/uhn/fhir/to/Controller.java | 61 ++-- .../webapp/WEB-INF/templates/resource.html | 96 ++++-- pom.xml | 2 +- 43 files changed, 1633 insertions(+), 475 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/BasicAuthInterceptor.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/CapturingInterceptor.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/LoggingInterceptor.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ICreate.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ICreateTyped.java create mode 100644 hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/InterceptorTest.java create mode 100644 hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/CreateTest.java create mode 100644 hapi-fhir-jpaserver-uhnfhirtest/src/test/java/ca/uhn/fhirtest/PopulateProfiles.java diff --git a/hapi-fhir-base/.classpath b/hapi-fhir-base/.classpath index 550e3f2ae04..123df06e860 100644 --- a/hapi-fhir-base/.classpath +++ b/hapi-fhir-base/.classpath @@ -6,6 +6,7 @@ + diff --git a/hapi-fhir-base/.settings/org.eclipse.wst.common.component b/hapi-fhir-base/.settings/org.eclipse.wst.common.component index 1a94c8cf11d..30be1c9ba76 100644 --- a/hapi-fhir-base/.settings/org.eclipse.wst.common.component +++ b/hapi-fhir-base/.settings/org.eclipse.wst.common.component @@ -2,5 +2,6 @@ + diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 11efafb28c6..82f53182d9b 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -76,7 +76,7 @@ org.slf4j slf4j-api - 1.7.6 + ${slf4j_version} ch.qos.logback diff --git a/hapi-fhir-base/src/changes/changes.xml b/hapi-fhir-base/src/changes/changes.xml index 31707633742..b04ebac4ec1 100644 --- a/hapi-fhir-base/src/changes/changes.xml +++ b/hapi-fhir-base/src/changes/changes.xml @@ -14,6 +14,19 @@ Search parameters are not properly escaped and unescaped. E.g. for a token parameter such as "&identifier=system|codepart1\|codepart2" + + Add support for OPTIONS verb (which returns the server conformance statement) + + + Add support for CORS headers in server + + + Bump SLF4j dependency to latest version (1.7.7) + + + Add interceptor framework for clients (annotation based and generic), and add interceptors + for configurable logging, capturing requests and responses, and HTTP basic auth. + diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BaseResource.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BaseResource.java index 75c2afb2b8f..82a7d403e08 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BaseResource.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/BaseResource.java @@ -127,7 +127,7 @@ public abstract class BaseResource extends BaseElement implements IResource { */ @Override protected boolean isBaseEmpty() { - return super.isBaseEmpty() && ElementUtil.isEmpty(myLanguage, myText); + return super.isBaseEmpty() && ElementUtil.isEmpty(myLanguage, myText, myId); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java index 04f2c940c94..226cc89f696 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java @@ -179,7 +179,8 @@ public class JsonParser extends BaseParser implements IParser { for (BundleEntry nextEntry : theBundle.getEntries()) { eventWriter.writeStartObject(); - if (nextEntry.getDeletedAt() !=null&&nextEntry.getDeletedAt().isEmpty()==false) { + boolean deleted = nextEntry.getDeletedAt() !=null&&nextEntry.getDeletedAt().isEmpty()==false; + if (deleted) { writeTagWithTextNode(eventWriter, "deleted", nextEntry.getDeletedAt()); } writeTagWithTextNode(eventWriter, "title", nextEntry.getTitle()); @@ -210,7 +211,7 @@ public class JsonParser extends BaseParser implements IParser { writeAuthor(nextEntry, eventWriter); IResource resource = nextEntry.getResource(); - if (resource != null && !resource.isEmpty()) { + if (resource != null && !resource.isEmpty() && !deleted) { RuntimeResourceDefinition resDef = myContext.getResourceDefinition(resource); encodeResourceToJsonStreamWriter(resDef, resource, eventWriter, "content", false); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java index 704a1f7c0ef..ba5e38ea176 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java @@ -199,7 +199,7 @@ public class XmlParser extends BaseParser implements IParser { } IResource resource = nextEntry.getResource(); - if (resource != null && !resource.isEmpty()) { + if (resource != null && !resource.isEmpty() && !deleted) { eventWriter.writeStartElement("content"); eventWriter.writeAttribute("type", "text/xml"); encodeResourceToXmlStreamWriter(resource, eventWriter, false); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/ClientInvocationHandler.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/ClientInvocationHandler.java index 9c0c9e82832..811a06d823e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/ClientInvocationHandler.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/ClientInvocationHandler.java @@ -27,6 +27,7 @@ import java.util.Map; import org.apache.http.client.HttpClient; +import ch.qos.logback.core.pattern.util.RegularEscapeUtil; import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.client.api.IRestfulClient; @@ -51,6 +52,8 @@ public class ClientInvocationHandler extends BaseClient implements InvocationHan myMethodToLambda.put(theClientType.getMethod("setEncoding", EncodingEnum.class), new SetEncodingLambda()); myMethodToLambda.put(theClientType.getMethod("setPrettyPrint", boolean.class), new SetPrettyPrintLambda()); + myMethodToLambda.put(theClientType.getMethod("registerInterceptor", IClientInterceptor.class), new RegisterInterceptorLambda()); + myMethodToLambda.put(theClientType.getMethod("unregisterInterceptor", IClientInterceptor.class), new UnregisterInterceptorLambda()); } catch (NoSuchMethodException e) { throw new ConfigurationException("Failed to find methods on client. This is a HAPI bug!", e); @@ -105,5 +108,22 @@ public class ClientInvocationHandler extends BaseClient implements InvocationHan return null; } } + private class UnregisterInterceptorLambda implements ILambda { + @Override + public Object handle(Object[] theArgs) { + IClientInterceptor interceptor = (IClientInterceptor) theArgs[0]; + unregisterInterceptor(interceptor); + return null; + } + } + + private class RegisterInterceptorLambda implements ILambda { + @Override + public Object handle(Object[] theArgs) { + IClientInterceptor interceptor = (IClientInterceptor) theArgs[0]; + registerInterceptor(interceptor); + return null; + } + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java index 67e3588c867..d72c60f5fd2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/GenericClient.java @@ -26,12 +26,14 @@ import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpRequestBase; @@ -49,6 +51,8 @@ import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; import ca.uhn.fhir.rest.gclient.IClientExecutable; +import ca.uhn.fhir.rest.gclient.ICreate; +import ca.uhn.fhir.rest.gclient.ICreateTyped; import ca.uhn.fhir.rest.gclient.ICriterion; import ca.uhn.fhir.rest.gclient.ICriterionInternal; import ca.uhn.fhir.rest.gclient.IGetPage; @@ -77,6 +81,7 @@ import ca.uhn.fhir.rest.method.ValidateMethodBinding; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; public class GenericClient extends BaseClient implements IGenericClient { @@ -106,6 +111,11 @@ public class GenericClient extends BaseClient implements IGenericClient { return resp; } + @Override + public ICreate create() { + return new CreateInternal(); + } + @Override public MethodOutcome create(IResource theResource) { BaseHttpClientInvocation invocation = CreateMethodBinding.createCreateInvocation(theResource, myContext); @@ -150,11 +160,6 @@ public class GenericClient extends BaseClient implements IGenericClient { return new GetTagsInternal(); } - @Override - public ITransaction transaction() { - return new TransactionInternal(); - } - @Override public Bundle history(final Class theType, IdDt theIdDt, DateTimeDt theSince, Integer theLimit) { String resourceName = theType != null ? toResourceName(theType) : null; @@ -179,12 +184,17 @@ public class GenericClient extends BaseClient implements IGenericClient { return myLogRequestAndResponse; } + @Override + public IGetPage loadPage() { + return new LoadPageInternal(); + } + @Override public T read(final Class theType, IdDt theId) { if (theId == null || theId.hasIdPart() == false) { throw new IllegalArgumentException("theId does not contain a valid ID, is: " + theId); } - + HttpGetClientInvocation invocation = ReadMethodBinding.createReadInvocation(theId, toResourceName(theType)); if (isKeepResponses()) { myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding()); @@ -241,6 +251,15 @@ public class GenericClient extends BaseClient implements IGenericClient { myLogRequestAndResponse = theLogRequestAndResponse; } + private String toResourceName(Class theType) { + return myContext.getResourceDefinition(theType).getName(); + } + + @Override + public ITransaction transaction() { + return new TransactionInternal(); + } + @Override public List transaction(List theResources) { BaseHttpClientInvocation invocation = TransactionMethodBinding.createTransactionInvocation(theResources, myContext); @@ -305,15 +324,18 @@ public class GenericClient extends BaseClient implements IGenericClient { return vread(theType, new IdDt(theId), new IdDt(theVersionId)); } - private String toResourceName(Class theType) { - return myContext.getResourceDefinition(theType).getName(); - } - private abstract class BaseClientExecutable, Y> implements IClientExecutable { private EncodingEnum myParamEncoding; private Boolean myPrettyPrint; private boolean myQueryLogRequestAndResponse; + protected void addParam(Map> params, String parameterName, String parameterValue) { + if (!params.containsKey(parameterName)) { + params.put(parameterName, new ArrayList()); + } + params.get(parameterName).add(parameterValue); + } + @SuppressWarnings("unchecked") @Override public T andLogRequestAndResponse(boolean theLogRequestAndResponse) { @@ -328,26 +350,13 @@ public class GenericClient extends BaseClient implements IGenericClient { return (T) this; } + @SuppressWarnings("unchecked") @Override public T encodedXml() { myParamEncoding = EncodingEnum.XML; return (T) this; } - @SuppressWarnings("unchecked") - @Override - public T prettyPrint() { - myPrettyPrint = true; - return (T) this; - } - - protected void addParam(Map> params, String parameterName, String parameterValue) { - if (!params.containsKey(parameterName)) { - params.put(parameterName, new ArrayList()); - } - params.get(parameterName).add(parameterValue); - } - protected Z invoke(Map> theParams, IClientResponseHandler theHandler, BaseHttpClientInvocation theInvocation) { if (myParamEncoding != null) { theParams.put(Constants.PARAM_FORMAT, Collections.singletonList(myParamEncoding.getFormatContentType())); @@ -365,6 +374,13 @@ public class GenericClient extends BaseClient implements IGenericClient { return resp; } + @SuppressWarnings("unchecked") + @Override + public T prettyPrint() { + myPrettyPrint = true; + return (T) this; + } + } private final class BundleResponseHandler implements IClientResponseHandler { @@ -376,7 +392,8 @@ public class GenericClient extends BaseClient implements IGenericClient { } @Override - public Bundle invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { + public Bundle invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, + BaseServerResponseException { EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); if (respType == null) { throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader); @@ -385,19 +402,71 @@ public class GenericClient extends BaseClient implements IGenericClient { return parser.parseBundle(myType, theResponseReader); } } - - private final class ResourceListResponseHandler implements IClientResponseHandler> { - private Class myType; + private class CreateInternal extends BaseClientExecutable implements ICreate, ICreateTyped { + + private String myId; + private IResource myResource; + private String myResourceBody; + + @Override + public MethodOutcome execute() { + if (myResource == null) { + EncodingEnum encoding = null; + for (int i = 0; i < myResourceBody.length() && encoding == null; i++) { + switch (myResourceBody.charAt(i)) { + case '<': + encoding = EncodingEnum.XML; + break; + case '{': + encoding = EncodingEnum.JSON; + break; + } + } + if (encoding == null) { + throw new InvalidRequestException("FHIR client can't determine resource encoding"); + } + myResource = encoding.newParser(myContext).parseResource(myResourceBody); + } + + BaseHttpClientInvocation invocation = CreateMethodBinding.createCreateInvocation(myResource,myResourceBody, myId, myContext); + + RuntimeResourceDefinition def = myContext.getResourceDefinition(myResource); + final String resourceName = def.getName(); + + OutcomeResponseHandler binding = new OutcomeResponseHandler(resourceName); + + Map> params = new HashMap>(); + return invoke(params, binding, invocation); - public ResourceListResponseHandler(Class theType) { - myType = theType; } @Override - public List invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { - return new BundleResponseHandler(myType).invokeClient(theResponseMimeType, theResponseReader, theResponseStatusCode, theHeaders).toListOfResources(); + public ICreateTyped resource(IResource theResource) { + Validate.notNull(theResource, "Resource can not be null"); + myResource = theResource; + return this; } + + @Override + public ICreateTyped resource(String theResourceBody) { + Validate.notBlank(theResourceBody, "Body can not be null or blank"); + myResourceBody = theResourceBody; + return this; + } + + @Override + public CreateInternal withId(IdDt theId) { + myId = theId.getIdPart(); + return this; + } + + @Override + public CreateInternal withId(String theId) { + myId = theId; + return this; + } + } private class ForInternal extends BaseClientExecutable implements IQuery { @@ -496,10 +565,31 @@ public class GenericClient extends BaseClient implements IGenericClient { } + private class GetPageInternal extends BaseClientExecutable implements IGetPageTyped { + + private String myUrl; + + public GetPageInternal(String theUrl) { + myUrl = theUrl; + } + + @Override + public Bundle execute() { + + BundleResponseHandler binding = new BundleResponseHandler(null); + HttpSimpleGetClientInvocation invocation = new HttpSimpleGetClientInvocation(myUrl); + + Map> params = null; + return invoke(params, binding, invocation); + + } + + } + private class GetTagsInternal extends BaseClientExecutable implements IGetTags { - private String myResourceName; private String myId; + private String myResourceName; private String myVersionId; @Override @@ -537,14 +627,6 @@ public class GenericClient extends BaseClient implements IGenericClient { return this; } - private void setResourceClass(Class theClass) { - if (theClass != null) { - myResourceName = myContext.getResourceDefinition(theClass).getName(); - } else { - myResourceName = null; - } - } - @Override public IGetTags forResource(Class theClass, String theId) { setResourceClass(theClass); @@ -560,6 +642,33 @@ public class GenericClient extends BaseClient implements IGenericClient { return this; } + private void setResourceClass(Class theClass) { + if (theClass != null) { + myResourceName = myContext.getResourceDefinition(theClass).getName(); + } else { + myResourceName = null; + } + } + + } + + private final class LoadPageInternal implements IGetPage { + + @Override + public IGetPageTyped next(Bundle theBundle) { + return new GetPageInternal(theBundle.getLinkNext().getValue()); + } + + @Override + public IGetPageTyped previous(Bundle theBundle) { + return new GetPageInternal(theBundle.getLinkPrevious().getValue()); + } + + @Override + public IGetPageTyped url(String thePageUrl) { + return new GetPageInternal(thePageUrl); + } + } private final class OutcomeResponseHandler implements IClientResponseHandler { @@ -570,7 +679,8 @@ public class GenericClient extends BaseClient implements IGenericClient { } @Override - public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { + public MethodOutcome invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, + BaseServerResponseException { MethodOutcome response = BaseOutcomeReturningMethodBinding.process2xxResponse(myContext, myResourceName, theResponseStatusCode, theResponseMimeType, theResponseReader, theHeaders); return response; } @@ -595,14 +705,29 @@ public class GenericClient extends BaseClient implements IGenericClient { } + private final class ResourceListResponseHandler implements IClientResponseHandler> { + + private Class myType; + + public ResourceListResponseHandler(Class theType) { + myType = theType; + } + + @Override + public List invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, + BaseServerResponseException { + return new BundleResponseHandler(myType).invokeClient(theResponseMimeType, theResponseReader, theResponseStatusCode, theHeaders).toListOfResources(); + } + } + private final class ResourceResponseHandler implements IClientResponseHandler { - private Class myType; private IdDt myId; + private Class myType; public ResourceResponseHandler(Class theType, IdDt theId) { myType = theType; - myId=theId; + myId = theId; } @Override @@ -613,11 +738,11 @@ public class GenericClient extends BaseClient implements IGenericClient { } IParser parser = respType.newParser(myContext); T retVal = parser.parseResource(myType, theResponseReader); - + if (myId != null) { retVal.setId(myId); } - + return retVal; } } @@ -666,7 +791,8 @@ public class GenericClient extends BaseClient implements IGenericClient { private final class TagListResponseHandler implements IClientResponseHandler { @Override - public TagList invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, BaseServerResponseException { + public TagList invokeClient(String theResponseMimeType, Reader theResponseReader, int theResponseStatusCode, Map> theHeaders) throws IOException, + BaseServerResponseException { EncodingEnum respType = EncodingEnum.forContentType(theResponseMimeType); if (respType == null) { throw NonFhirResponseException.newInstance(theResponseStatusCode, theResponseMimeType, theResponseReader); @@ -676,94 +802,47 @@ public class GenericClient extends BaseClient implements IGenericClient { } } - @Override - public IGetPage loadPage() { - return new LoadPageInternal(); - } + private final class TransactionExecutable extends BaseClientExecutable, T> implements ITransactionTyped { - private final class LoadPageInternal implements IGetPage { + private Bundle myBundle; + private List myResources; - @Override - public IGetPageTyped previous(Bundle theBundle) { - return new GetPageInternal(theBundle.getLinkPrevious().getValue()); + public TransactionExecutable(Bundle theResources) { + myBundle = theResources; } - @Override - public IGetPageTyped next(Bundle theBundle) { - return new GetPageInternal(theBundle.getLinkNext().getValue()); + public TransactionExecutable(List theResources) { + myResources = theResources; } + @SuppressWarnings("unchecked") @Override - public IGetPageTyped url(String thePageUrl) { - return new GetPageInternal(thePageUrl); + public T execute() { + if (myResources != null) { + ResourceListResponseHandler binding = new ResourceListResponseHandler(null); + BaseHttpClientInvocation invocation = TransactionMethodBinding.createTransactionInvocation(myResources, myContext); + Map> params = null; + return (T) invoke(params, binding, invocation); + } else { + BundleResponseHandler binding = new BundleResponseHandler(null); + BaseHttpClientInvocation invocation = TransactionMethodBinding.createTransactionInvocation(myBundle, myContext); + Map> params = null; + return (T) invoke(params, binding, invocation); + } } } private final class TransactionInternal implements ITransaction { - @Override - public ITransactionTyped> withResources(List theResources) { - return new TransactionExecutable>(theResources); - } - @Override public ITransactionTyped withBundle(Bundle theResources) { return new TransactionExecutable(theResources); } - - } - - - private final class TransactionExecutable extends BaseClientExecutable, T> implements ITransactionTyped{ - - private List myResources; - private Bundle myBundle; - - public TransactionExecutable(List theResources) { - myResources=theResources; - } - - public TransactionExecutable(Bundle theResources) { - myBundle=theResources; - } - - @SuppressWarnings("unchecked") @Override - public T execute() { - if (myResources!=null) { - ResourceListResponseHandler binding = new ResourceListResponseHandler(null); - BaseHttpClientInvocation invocation = TransactionMethodBinding.createTransactionInvocation(myResources, myContext); - Map> params = null; - return (T) invoke(params, binding, invocation); - }else { - BundleResponseHandler binding = new BundleResponseHandler(null); - BaseHttpClientInvocation invocation = TransactionMethodBinding.createTransactionInvocation(myBundle, myContext); - Map> params = null; - return (T) invoke(params, binding, invocation); - } - } - - } - - private class GetPageInternal extends BaseClientExecutable implements IGetPageTyped { - - private String myUrl; - - public GetPageInternal(String theUrl) { - myUrl = theUrl; - } - - @Override - public Bundle execute() { - - BundleResponseHandler binding = new BundleResponseHandler(null); - HttpSimpleGetClientInvocation invocation = new HttpSimpleGetClientInvocation(myUrl); - - Map> params = null; - return invoke(params, binding, invocation); - + public ITransactionTyped> withResources(List theResources) { + return new TransactionExecutable>(theResources); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/HttpBasicAuthInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/HttpBasicAuthInterceptor.java index 56062675dd4..1bfb506f45d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/HttpBasicAuthInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/HttpBasicAuthInterceptor.java @@ -1,5 +1,19 @@ package ca.uhn.fhir.rest.client; +import java.io.IOException; + +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.auth.AuthState; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.protocol.HttpContext; + +import ca.uhn.fhir.rest.client.api.IBasicClient; + /* * #%L * HAPI FHIR Library @@ -20,27 +34,11 @@ package ca.uhn.fhir.rest.client; * #L% */ -import java.io.IOException; - -import org.apache.http.HttpException; -import org.apache.http.HttpRequest; -import org.apache.http.HttpRequestInterceptor; -import org.apache.http.auth.AuthState; -import org.apache.http.auth.Credentials; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.protocol.HttpClientContext; -import org.apache.http.impl.auth.BasicScheme; -import org.apache.http.protocol.HttpContext; /** - * HTTP interceptor to be used for adding HTTP basic auth username/password tokens - * to requests - *

- * See the HAPI Documentation - * for information on how to use this class. - *

+ * @deprecated Use {@link ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor} instead. Note that that class is a HAPI client interceptor instead of being a commons-httpclient interceptor, so you register it to your client instance once it's created using {@link IGenericClient#registerInterceptor(IClientInterceptor)} or {@link IBasicClient#registerInterceptor(IClientInterceptor)} instead */ -public class HttpBasicAuthInterceptor implements HttpRequestInterceptor { +public class HttpBasicAuthInterceptor implements HttpRequestInterceptor { private String myUsername; private String myPassword; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IClientInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IClientInterceptor.java index 7029cc05589..a1472b2f4f2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IClientInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IClientInterceptor.java @@ -20,13 +20,21 @@ package ca.uhn.fhir.rest.client; * #L% */ +import java.io.IOException; + import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpRequestBase; public interface IClientInterceptor { + /** + * Fired by the client just before invoking the HTTP client request + */ void interceptRequest(HttpRequestBase theRequest); - void interceptResponse(HttpResponse theRequest); + /** + * Fired by the client upon receiving an HTTP response, prior to processing that response + */ + void interceptResponse(HttpResponse theResponse) throws IOException; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java index ec73de11f1b..980b0b7c201 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/IGenericClient.java @@ -30,13 +30,25 @@ import ca.uhn.fhir.model.dstu.resource.Conformance; import ca.uhn.fhir.model.primitive.DateTimeDt; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.client.api.IRestfulClient; +import ca.uhn.fhir.rest.gclient.ICreate; import ca.uhn.fhir.rest.gclient.IGetPage; import ca.uhn.fhir.rest.gclient.IGetTags; import ca.uhn.fhir.rest.gclient.ITransaction; import ca.uhn.fhir.rest.gclient.IUntypedQuery; public interface IGenericClient { + /** + * Register a new interceptor for this client. An interceptor can be used to add additional + * logging, or add security headers, or pre-process responses, etc. + */ + void registerInterceptor(IClientInterceptor theInterceptor); + /** + * Remove an intercaptor that was previously registered using {@link IRestfulClient#registerInterceptor(IClientInterceptor)} + */ + void unregisterInterceptor(IClientInterceptor theInterceptor); + /** * Retrieves and returns the server conformance statement */ @@ -243,4 +255,9 @@ public interface IGenericClient { */ ITransaction transaction(); + /** + * Fluent method for the "create" operation, which creates a new resource instance on the server + */ + ICreate create(); + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClient.java index 405025f5634..abdaa658994 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClient.java @@ -21,9 +21,11 @@ package ca.uhn.fhir.rest.client.api; */ +import org.apache.commons.lang3.Validate; import org.apache.http.client.HttpClient; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.IClientInterceptor; import ca.uhn.fhir.rest.server.EncodingEnum; public interface IRestfulClient { @@ -64,6 +66,15 @@ public interface IRestfulClient { */ String getServerBase(); - + /** + * Register a new interceptor for this client. An interceptor can be used to add additional + * logging, or add security headers, or pre-process responses, etc. + */ + void registerInterceptor(IClientInterceptor theInterceptor); + + /** + * Remove an intercaptor that was previously registered using {@link IRestfulClient#registerInterceptor(IClientInterceptor)} + */ + void unregisterInterceptor(IClientInterceptor theInterceptor); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/BasicAuthInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/BasicAuthInterceptor.java new file mode 100644 index 00000000000..f63a8e43d20 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/BasicAuthInterceptor.java @@ -0,0 +1,73 @@ +package ca.uhn.fhir.rest.client.interceptor; + +/* + * #%L + * HAPI FHIR Library + * %% + * Copyright (C) 2014 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpRequestBase; + +import ca.uhn.fhir.rest.client.IClientInterceptor; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; + +/** + * HTTP interceptor to be used for adding HTTP basic auth username/password tokens + * to requests + *

+ * See the HAPI Documentation + * for information on how to use this class. + *

+ */ +public class BasicAuthInterceptor implements IClientInterceptor { + + private String myUsername; + private String myPassword; + + public BasicAuthInterceptor(String theUsername, String thePassword) { + super(); + myUsername = theUsername; + myPassword = thePassword; + } + + @Override + public void interceptRequest(HttpRequestBase theRequest) { + String authorizationUnescaped = StringUtils.defaultString(myUsername) + ":" + StringUtils.defaultString(myPassword); + String encoded; + try { + encoded = Base64.encodeBase64String(authorizationUnescaped.getBytes("ISO-8859-1")); + } catch (UnsupportedEncodingException e) { + throw new InternalErrorException("Could not find US-ASCII encoding. This shouldn't happen!"); + } + theRequest.addHeader(Constants.HEADER_AUTHORIZATION, ("Basic " + encoded)); + } + + @Override + public void interceptResponse(HttpResponse theResponse) throws IOException { + // nothing + } + + + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/CapturingInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/CapturingInterceptor.java new file mode 100644 index 00000000000..db000733db1 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/CapturingInterceptor.java @@ -0,0 +1,43 @@ +package ca.uhn.fhir.rest.client.interceptor; + +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpRequestBase; + +import ca.uhn.fhir.rest.client.IClientInterceptor; + +/** + * Client interceptor which simply captures request and response objects and stored them so that they can be inspected after a client + * call has returned + */ +public class CapturingInterceptor implements IClientInterceptor { + + private HttpRequestBase myLastRequest; + private HttpResponse myLastResponse; + + /** + * Clear the last request and response values + */ + public void clear() { + myLastRequest = null; + myLastResponse = null; + } + + public HttpRequestBase getLastRequest() { + return myLastRequest; + } + + public HttpResponse getLastResponse() { + return myLastResponse; + } + + @Override + public void interceptRequest(HttpRequestBase theRequest) { + myLastRequest = theRequest; + } + + @Override + public void interceptResponse(HttpResponse theRequest) { + myLastResponse = theRequest; + } + +} \ No newline at end of file diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/LoggingInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/LoggingInterceptor.java new file mode 100644 index 00000000000..c957beb91ce --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/interceptor/LoggingInterceptor.java @@ -0,0 +1,184 @@ +package ca.uhn.fhir.rest.client.interceptor; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.HttpEntityWrapper; + +import ca.uhn.fhir.rest.client.IClientInterceptor; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; + +public class LoggingInterceptor implements IClientInterceptor { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LoggingInterceptor.class); + private boolean myLogRequestSummary = true; + private boolean myLogResponseSummary = true; + private boolean myLogResponseBody = false; + private boolean myLogRequestBody = false; + private boolean myLogResponseHeaders=false; + private boolean myLogRequestHeaders=false; + + /** + * Constructor + */ + public LoggingInterceptor() { + super(); + } + + /** + * Constructor + * + * @param theVerbose If set to true, all logging is enabled + */ + public LoggingInterceptor(boolean theVerbose) { + if (theVerbose) { + setLogRequestBody(true); + setLogRequestSummary(true); + setLogResponseBody(true); + setLogResponseSummary(true); + setLogRequestHeaders(true); + setLogResponseHeaders(true); + } + } + + @Override + public void interceptRequest(HttpRequestBase theRequest) { + if (myLogRequestSummary) { + ourLog.info("Client request: {}", theRequest); + } + + if (myLogRequestHeaders) { + StringBuilder b = new StringBuilder(); + for (int i = 0;i < theRequest.getAllHeaders().length;i++) { + Header next = theRequest.getAllHeaders()[i]; + b.append(next.getName() + ": " + next.getValue()); + if(i+1 < theRequest.getAllHeaders().length) { + b.append('\n'); + } + } + ourLog.info("Client request headers:\n{}", b.toString()); + } + + if (myLogRequestBody) { + if (theRequest instanceof HttpEntityEnclosingRequest) { + HttpEntity entity = ((HttpEntityEnclosingRequest) theRequest).getEntity(); + if (entity.isRepeatable()) { + try { + String content = IOUtils.toString(entity.getContent()); + ourLog.info("Client request body:\n{}", content); + } catch (IllegalStateException e) { + ourLog.warn("Failed to replay request contents (during logging attempt, actual FHIR call did not fail)", e); + } catch (IOException e) { + ourLog.warn("Failed to replay request contents (during logging attempt, actual FHIR call did not fail)", e); + } + } + } + } + + } + + @Override + public void interceptResponse(HttpResponse theResponse) throws IOException { + if (myLogResponseSummary) { + String message = "HTTP " + theResponse.getStatusLine().getStatusCode() + " " + theResponse.getStatusLine().getReasonPhrase(); + ourLog.info("Client response: {}", message); + } + + if (myLogRequestHeaders) { + StringBuilder b = new StringBuilder(); + for (int i = 0;i < theResponse.getAllHeaders().length;i++) { + Header next = theResponse.getAllHeaders()[i]; + b.append(next.getName() + ": " + next.getValue()); + if(i+1 < theResponse.getAllHeaders().length) { + b.append('\n'); + } + } + ourLog.info("Client response headers:\n{}", b.toString()); + } + + if (myLogResponseBody) { + HttpEntity respEntity = theResponse.getEntity(); + final byte[] bytes; + try { + bytes = IOUtils.toByteArray(respEntity.getContent()); + } catch (IllegalStateException e) { + throw new InternalErrorException(e); + } + + ourLog.info("Client response body:\n{}", new String(bytes)); + + theResponse.setEntity(new MyEntityWrapper(respEntity, bytes)); + } + } + + private static class MyEntityWrapper extends HttpEntityWrapper { + + private byte[] myBytes; + + public MyEntityWrapper(HttpEntity theWrappedEntity, byte[] theBytes) { + super(theWrappedEntity); + myBytes = theBytes; + } + + @Override + public InputStream getContent() throws IOException { + return new ByteArrayInputStream(myBytes); + } + + @Override + public void writeTo(OutputStream theOutstream) throws IOException { + theOutstream.write(myBytes); + } + + } + + /** + * Should a summary (one line) for each request be logged, containing the URL and other information + */ + public void setLogRequestSummary(boolean theValue) { + myLogRequestSummary = theValue; + } + + /** + * Should a summary (one line) for each request be logged, containing the URL and other information + */ + public void setLogResponseSummary(boolean theValue) { + myLogResponseSummary = theValue; + } + + /** + * Should headers for each request be logged, containing the URL and other information + */ + public void setLogRequestHeaders(boolean theValue) { + myLogRequestHeaders = theValue; + } + + /** + * Should headers for each request be logged, containing the URL and other information + */ + public void setLogResponseHeaders(boolean theValue) { + myLogResponseHeaders = theValue; + } + + /** + * Should a summary (one line) for each request be logged, containing the URL and other information + */ + public void setLogRequestBody(boolean theValue) { + myLogRequestBody = theValue; + } + + /** + * Should a summary (one line) for each request be logged, containing the URL and other information + */ + public void setLogResponseBody(boolean theValue) { + myLogResponseBody = theValue; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ICreate.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ICreate.java new file mode 100644 index 00000000000..8b16ed3d09a --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ICreate.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.rest.gclient; + +/* + * #%L + * HAPI FHIR Library + * %% + * Copyright (C) 2014 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.model.api.IResource; + +public interface ICreate { + + ICreateTyped resource(IResource theResource); + + ICreateTyped resource(String theResourceAsText); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ICreateTyped.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ICreateTyped.java new file mode 100644 index 00000000000..8fd1d56a728 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/ICreateTyped.java @@ -0,0 +1,40 @@ +package ca.uhn.fhir.rest.gclient; + +/* + * #%L + * HAPI FHIR Library + * %% + * Copyright (C) 2014 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.MethodOutcome; + +public interface ICreateTyped extends IClientExecutable { + + /** + * If you want the explicitly state an ID for your created resource, put that ID here. You generally do not + * need to invoke this method, so that the server will assign the ID itself. + */ + ICreateTyped withId(String theId); + + /** + * If you want the explicitly state an ID for your created resource, put that ID here. You generally do not + * need to invoke this method, so that the server will assign the ID itself. + */ + ICreateTyped withId(IdDt theId); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseHttpClientInvocationWithContents.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseHttpClientInvocationWithContents.java index fa84a7ff0c8..8fc5fcfd82a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseHttpClientInvocationWithContents.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseHttpClientInvocationWithContents.java @@ -50,7 +50,8 @@ public abstract class BaseHttpClientInvocationWithContents extends BaseHttpClien private final TagList myTagList; private final List myResources; private final Bundle myBundle; - + private final String myContents; + public BaseHttpClientInvocationWithContents(FhirContext theContext, IResource theResource, String theUrlExtension) { super(); myContext = theContext; @@ -59,6 +60,7 @@ public abstract class BaseHttpClientInvocationWithContents extends BaseHttpClien myTagList = null; myResources = null; myBundle = null; + myContents = null; } public BaseHttpClientInvocationWithContents(FhirContext theContext, TagList theTagList, String... theUrlExtension) { @@ -72,6 +74,7 @@ public abstract class BaseHttpClientInvocationWithContents extends BaseHttpClien myTagList = theTagList; myResources = null; myBundle = null; + myContents = null; myUrlExtension = StringUtils.join(theUrlExtension, '/'); } @@ -83,6 +86,7 @@ public abstract class BaseHttpClientInvocationWithContents extends BaseHttpClien myUrlExtension = null; myResources = theResources; myBundle = null; + myContents = null; } public BaseHttpClientInvocationWithContents(FhirContext theContext, Bundle theBundle) { @@ -92,8 +96,20 @@ public abstract class BaseHttpClientInvocationWithContents extends BaseHttpClien myUrlExtension = null; myResources = null; myBundle = theBundle; + myContents = null; } + public BaseHttpClientInvocationWithContents(FhirContext theContext, String theContents, String theUrlExtension) { + myContext = theContext; + myResource = null; + myTagList = null; + myUrlExtension = theUrlExtension; + myResources = null; + myBundle = null; + myContents = theContents; + } + + @Override public HttpRequestBase asHttpRequest(String theUrlBase, Map> theExtraParams, EncodingEnum theEncoding) throws DataFormatException { StringBuilder b = new StringBuilder(); @@ -131,6 +147,8 @@ public abstract class BaseHttpClientInvocationWithContents extends BaseHttpClien } else if (myResources != null) { Bundle bundle = RestfulServer.createBundleFromResourceList(myContext, "", myResources, "", "", myResources.size()); contents = parser.encodeBundleToString(bundle); + } else if (myContents != null) { + contents = myContents; } else { contents = parser.encodeResourceToString(myResource); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CreateMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CreateMethodBinding.java index e084de69268..15519f34770 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CreateMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CreateMethodBinding.java @@ -20,10 +20,13 @@ package ca.uhn.fhir.rest.method; * #L% */ +import java.io.IOException; import java.lang.reflect.Method; import java.util.Collections; import java.util.Set; +import org.apache.commons.lang3.StringUtils; + import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.model.api.IResource; @@ -55,12 +58,24 @@ public class CreateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe return Collections.singleton(RequestType.POST); } + @Override + protected IResource parseIncomingServerResource(Request theRequest) throws IOException { + IResource retVal = super.parseIncomingServerResource(theRequest); + + if (theRequest.getId() != null && theRequest.getId().hasIdPart()) { + retVal.setId(theRequest.getId()); + } + + return retVal; + } + + @Override protected BaseHttpClientInvocation createClientInvocation(Object[] theArgs, IResource theResource) { FhirContext context = getContext(); BaseHttpClientInvocation retVal = createCreateInvocation(theResource, context); - + if (theArgs != null) { for (int idx = 0; idx < theArgs.length; idx++) { IParameter nextParam = getParameters().get(idx); @@ -72,16 +87,28 @@ public class CreateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe } public static HttpPostClientInvocation createCreateInvocation(IResource theResource, FhirContext theContext) { + return createCreateInvocation(theResource, null,null, theContext); + } + + public static HttpPostClientInvocation createCreateInvocation(IResource theResource, String theResourceBody, String theId, FhirContext theContext) { RuntimeResourceDefinition def = theContext.getResourceDefinition(theResource); String resourceName = def.getName(); - StringBuilder urlExtension = new StringBuilder(); urlExtension.append(resourceName); + if (StringUtils.isNotBlank(theId)) { + urlExtension.append('/'); + urlExtension.append(theId); + } - HttpPostClientInvocation retVal = new HttpPostClientInvocation(theContext, theResource, urlExtension.toString()); + HttpPostClientInvocation retVal; + if (StringUtils.isBlank(theResourceBody)) { + retVal = new HttpPostClientInvocation(theContext, theResource, urlExtension.toString()); + }else { + retVal = new HttpPostClientInvocation(theContext, theResourceBody, urlExtension.toString()); + } addTagsToPostOrPut(theResource, retVal); - + return retVal; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpPostClientInvocation.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpPostClientInvocation.java index 7fc83f4cab6..2ab7222e8c0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpPostClientInvocation.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpPostClientInvocation.java @@ -51,6 +51,10 @@ public class HttpPostClientInvocation extends BaseHttpClientInvocationWithConten super(theContext,theBundle); } + public HttpPostClientInvocation(FhirContext theContext, String theContents, String theUrlExtension) { + super(theContext,theContents, theUrlExtension); + } + @Override protected HttpPost createRequest(String url, AbstractHttpEntity theEntity) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java index 2d0d088ab18..030237da2fa 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/Constants.java @@ -28,6 +28,10 @@ import java.util.Set; public class Constants { + public static final String HEADER_CORS_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + public static final String HEADER_CORS_ALLOW_METHODS = "Access-Control-Allow-Methods"; + public static final String HEADER_CORS_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + public static final String CHARSET_UTF_8 = "UTF-8"; public static final String CT_ATOM_XML = "application/atom+xml"; public static final String CT_FHIR_JSON = "application/json+fhir"; @@ -88,6 +92,8 @@ public class Constants { public static final String ENCODING_GZIP = "gzip"; public static final String HEADER_LOCATION = "Location"; public static final String HEADER_LOCATION_LC = HEADER_LOCATION.toLowerCase(); + public static final String HEADERVALUE_CORS_ALLOW_METHODS_ALL = "GET, POST, PUT, DELETE"; + public static final String HEADER_AUTHORIZATION = "Authorization"; static { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index c923c6af42e..13cc91d798e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -79,6 +79,7 @@ import ca.uhn.fhir.util.VersionUtil; public class RestfulServer extends HttpServlet { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServer.class); private static final long serialVersionUID = 1L; @@ -99,6 +100,20 @@ public class RestfulServer extends HttpServlet { private String myServerVersion = VersionUtil.getVersion(); private boolean myStarted; private boolean myUseBrowserFriendlyContentTypes; + private String myCorsAllowDomain; + + public String getCorsAllowDomain() { + return myCorsAllowDomain; + } + + /** + * If set to anything other than null (which is the default), the server will return CORS (Cross Origin Resource Sharing) headers with the given domain string. + *

+ * A value of "*" indicates that the server allows access to all domains (which may be appropriate in development situations but is generally not appropriate in production) + */ + public void setCorsAllowDomain(String theCorsAllowDomain) { + myCorsAllowDomain = theCorsAllowDomain; + } /** * Constructor @@ -120,6 +135,13 @@ public class RestfulServer extends HttpServlet { */ public void addHeadersToResponse(HttpServletResponse theHttpResponse) { theHttpResponse.addHeader("X-Powered-By", "HAPI FHIR " + VersionUtil.getVersion() + " RESTful Server"); + + if (isNotBlank(myCorsAllowDomain)) { + theHttpResponse.addHeader(Constants.HEADER_CORS_ALLOW_ORIGIN, myCorsAllowDomain); + theHttpResponse.addHeader(Constants.HEADER_CORS_ALLOW_METHODS, Constants.HEADERVALUE_CORS_ALLOW_METHODS_ALL); + theHttpResponse.addHeader(Constants.HEADER_CORS_EXPOSE_HEADERS, Constants.HEADER_CONTENT_LOCATION); + } + } private void assertProviderIsValid(Object theNext) throws ConfigurationException { @@ -352,7 +374,7 @@ public class RestfulServer extends HttpServlet { int start = Math.min(offsetI, resultList.size() - 1); - EncodingEnum responseEncoding = determineRequestEncoding(theRequest); + EncodingEnum responseEncoding = determineResponseEncoding(theRequest.getServletRequest()); boolean prettyPrint = prettyPrintResponse(theRequest); boolean requestIsBrowser = requestIsBrowser(theRequest.getServletRequest()); NarrativeModeEnum narrativeMode = determineNarrativeMode(theRequest); @@ -418,7 +440,7 @@ public class RestfulServer extends HttpServlet { ResourceBinding resourceBinding = null; BaseMethodBinding resourceMethod = null; - if ("metadata".equals(resourceName)) { + if ("metadata".equals(resourceName) || theRequestType == RequestType.OPTIONS) { resourceMethod = myServerConformanceMethod; } else if (resourceName == null) { resourceBinding = myNullResourceBinding; diff --git a/hapi-fhir-base/src/site/example/java/example/ClientExamples.java b/hapi-fhir-base/src/site/example/java/example/ClientExamples.java index d6a66bf7ecf..ad8b1e2bbef 100644 --- a/hapi-fhir-base/src/site/example/java/example/ClientExamples.java +++ b/hapi-fhir-base/src/site/example/java/example/ClientExamples.java @@ -3,10 +3,11 @@ package example; import org.apache.http.impl.client.HttpClientBuilder; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.rest.client.HttpBasicAuthInterceptor; import ca.uhn.fhir.rest.client.IGenericClient; import ca.uhn.fhir.rest.client.IRestfulClientFactory; import ca.uhn.fhir.rest.client.api.IBasicClient; +import ca.uhn.fhir.rest.client.interceptor.BasicAuthInterceptor; +import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.server.EncodingEnum; public class ClientExamples { @@ -17,29 +18,50 @@ public class ClientExamples { @SuppressWarnings("unused") public void createSecurity() { -{ //START SNIPPET: security // Create a context and get the client factory so it can be configured FhirContext ctx = new FhirContext(); IRestfulClientFactory clientFactory = ctx.getRestfulClientFactory(); -// Create an HTTP Client Builder -HttpClientBuilder builder = HttpClientBuilder.create(); - -// This interceptor adds HTTP username/password to every request +//Create an HTTP basic auth interceptor String username = "foobar"; String password = "boobear"; -builder.addInterceptorFirst(new HttpBasicAuthInterceptor(username, password)); +BasicAuthInterceptor authInterceptor = new BasicAuthInterceptor(username, password); -// Use the new HTTP client builder -clientFactory.setHttpClient(builder.build()); - -// This factory is applied to both styles of client +// Register the interceptor with your client (either style) IPatientClient annotationClient = ctx.newRestfulClient(IPatientClient.class, "http://localhost:9999/fhir"); +annotationClient.registerInterceptor(authInterceptor); + IGenericClient genericClient = ctx.newRestfulGenericClient("http://localhost:9999/fhir"); +annotationClient.registerInterceptor(authInterceptor); //END SNIPPET: security } +@SuppressWarnings("unused") +public void createLogging() { +{ +//START SNIPPET: logging +//Create a context and get the client factory so it can be configured +FhirContext ctx = new FhirContext(); +IRestfulClientFactory clientFactory = ctx.getRestfulClientFactory(); + +//Create a logging interceptor +LoggingInterceptor loggingInterceptor = new LoggingInterceptor(); + +// Optionally you may configure the interceptor (by default only summary info is logged) +loggingInterceptor.setLogRequestSummary(true); +loggingInterceptor.setLogRequestBody(true); + +//Register the interceptor with your client (either style) +IPatientClient annotationClient = ctx.newRestfulClient(IPatientClient.class, "http://localhost:9999/fhir"); +annotationClient.registerInterceptor(loggingInterceptor); + +IGenericClient genericClient = ctx.newRestfulGenericClient("http://localhost:9999/fhir"); +annotationClient.registerInterceptor(loggingInterceptor); +//END SNIPPET: logging +} + + /******************************/ { diff --git a/hapi-fhir-base/src/site/xdoc/doc_rest_client.xml b/hapi-fhir-base/src/site/xdoc/doc_rest_client.xml index 144def9e1e0..f3edce85428 100644 --- a/hapi-fhir-base/src/site/xdoc/doc_rest_client.xml +++ b/hapi-fhir-base/src/site/xdoc/doc_rest_client.xml @@ -165,6 +165,28 @@ + + +

+ Restful client interfaces that you create will also extend + the interface + IRestfulClient, + which comes with some helpful methods for configuring the way that + the client will interact with the server. +

+

+ The following snippet shows how to configure the cliet to explicitly + request JSON or XML responses, and how to request "pretty printed" responses + on servers that support this (HAPI based servers currently). +

+ + + + + + + +

@@ -205,6 +227,21 @@ on the RestfulClientFactory.

+ + +

+ Both generic clients and annotation-driven clients support + Client Interceptors, + which may be registered in order to provide specific behaviour to each + client request. +

+ +

+ The following section shows some sample interceptors which may be used. +

+ +
+

@@ -219,23 +256,15 @@ - +

- Restful client interfaces that you create will also extend - the interface - IRestfulClient, - which comes with some helpful methods for configuring the way that - the client will interact with the server. + The following example shows how to configure your client to + use a specific username and password in every request.

-

- The following snippet shows how to configure the cliet to explicitly - request JSON or XML responses, and how to request "pretty printed" responses - on servers that support this (HAPI based servers currently). -

- + - + diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ClientTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ClientTest.java index 2728d70b09b..0f948ab55ec 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ClientTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/ClientTest.java @@ -22,6 +22,7 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicHttpResponse; import org.apache.http.message.BasicStatusLine; import org.hamcrest.core.StringContains; import org.hamcrest.core.StringEndsWith; @@ -57,6 +58,8 @@ import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.api.IBasicClient; +import ca.uhn.fhir.rest.client.interceptor.CapturingInterceptor; +import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; import ca.uhn.fhir.rest.param.CodingListParam; import ca.uhn.fhir.rest.param.DateRangeParam; import ca.uhn.fhir.rest.param.QualifiedDateParam; @@ -73,8 +76,6 @@ public class ClientTest { private HttpClient httpClient; private HttpResponse httpResponse; - // atom-document-large.xml - @Before public void before() { ctx = new FhirContext(Patient.class, Conformance.class); @@ -85,6 +86,36 @@ public class ClientTest { httpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); } + // atom-document-large.xml + + private String getPatientFeedWithOneResult() { + //@formatter:off + String msg = "\n" + + "\n" + + "<id>d039f91a-cc3c-4013-988e-af4d8d0614bd</id>\n" + + "<os:totalResults xmlns:os=\"http://a9.com/-/spec/opensearch/1.1/\">1</os:totalResults>\n" + + "<published>2014-03-11T16:35:07-04:00</published>\n" + + "<author>\n" + + "<name>ca.uhn.fhir.rest.server.DummyRestfulServer</name>\n" + + "</author>\n" + + "<entry>\n" + + "<content type=\"text/xml\">" + + "<Patient xmlns=\"http://hl7.org/fhir\">" + + "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>" + + "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>" + + "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>" + + "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>" + + "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>" + + "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>" + + "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />" + + "</Patient>" + + "</content>\n" + + " </entry>\n" + + "</feed>"; + //@formatter:on + return msg; + } + @Test public void testCreate() throws Exception { @@ -99,8 +130,13 @@ public class ClientTest { when(httpResponse.getAllHeaders()).thenReturn(toHeaderArray("Location", "http://example.com/fhir/Patient/100/_history/200")); ITestClient client = ctx.newRestfulClient(ITestClient.class, "http://foo"); + CapturingInterceptor interceptor = new CapturingInterceptor(); + client.registerInterceptor(interceptor); + MethodOutcome response = client.createPatient(patient); + assertEquals(interceptor.getLastRequest().getURI().toASCIIString(), "http://foo/Patient"); + assertEquals(HttpPost.class, capt.getValue().getClass()); HttpPost post = (HttpPost) capt.getValue(); assertThat(IOUtils.toString(post.getEntity().getContent()), StringContains.containsString("<Patient")); @@ -129,8 +165,7 @@ public class ClientTest { } /** - * Some servers (older ones?) return the resourcde you created instead of an - * OperationOutcome. We just need to ignore it. + * Some servers (older ones?) return the resourcde you created instead of an OperationOutcome. We just need to ignore it. */ @Test public void testCreateWithResourceResponse() throws Exception { @@ -562,6 +597,29 @@ public class ClientTest { } + @Test + public void testReadFailureInternalError() throws Exception { + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(httpClient.execute(capt.capture())).thenReturn(httpResponse); + when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 500, "INTERNAL")); + Header[] headers = new Header[1]; + headers[0] = new BasicHeader(Constants.HEADER_LAST_MODIFIED, "2011-01-02T22:01:02"); + when(httpResponse.getAllHeaders()).thenReturn(headers); + when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT)); + when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("Internal Failure"), Charset.forName("UTF-8"))); + + ITestClient client = ctx.newRestfulClient(ITestClient.class, "http://foo"); + try { + client.getPatientById(new IdDt("111")); + fail(); + } catch (InternalErrorException e) { + assertThat(e.getMessage(), containsString("INTERNAL")); + assertThat(e.getResponseBody(), containsString("Internal Failure")); + } + + } + @Test public void testReadFailureNoCharset() throws Exception { @@ -588,30 +646,6 @@ public class ClientTest { } - @Test - public void testReadFailureInternalError() throws Exception { - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(httpClient.execute(capt.capture())).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 500, "INTERNAL")); - Header[] headers = new Header[1]; - headers[0] = new BasicHeader(Constants.HEADER_LAST_MODIFIED, "2011-01-02T22:01:02"); - when(httpResponse.getAllHeaders()).thenReturn(headers); - when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_TEXT)); - when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader("Internal Failure"), Charset.forName("UTF-8"))); - - ITestClient client = ctx.newRestfulClient(ITestClient.class, "http://foo"); - try { - client.getPatientById(new IdDt("111")); - fail(); - } catch (InternalErrorException e) { - assertThat(e.getMessage(), containsString("INTERNAL")); - assertThat(e.getResponseBody(), containsString("Internal Failure")); - } - - } - - @Test public void testReadNoCharset() throws Exception { @@ -676,6 +710,28 @@ public class ClientTest { String msg = getPatientFeedWithOneResult(); + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + + when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); + +// httpResponse = new BasicHttpResponse(statusline, catalog, locale) + when(httpClient.execute(capt.capture())).thenReturn(httpResponse); + + ITestClient client = ctx.newRestfulClient(ITestClient.class, "http://foo"); + List<Patient> response = client.getPatientByDob(new QualifiedDateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); + + assertEquals("http://foo/Patient?birthdate=%3E%3D2011-01-02", capt.getValue().getURI().toString()); + assertEquals("PRP1660", response.get(0).getIdentifier().get(0).getValue().getValue()); + + } + + @Test + public void testSearchByQuantity() throws Exception { + + String msg = getPatientFeedWithOneResult(); + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); when(httpClient.execute(capt.capture())).thenReturn(httpResponse); when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); @@ -683,10 +739,10 @@ public class ClientTest { when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); ITestClient client = ctx.newRestfulClient(ITestClient.class, "http://foo"); - List<Patient> response = client.getPatientByDob(new QualifiedDateParam(QuantityCompararatorEnum.GREATERTHAN_OR_EQUALS, "2011-01-02")); + Patient response = client.findPatientQuantity(new QuantityDt(QuantityCompararatorEnum.GREATERTHAN, 123L, "foo", "bar")); - assertEquals("http://foo/Patient?birthdate=%3E%3D2011-01-02", capt.getValue().getURI().toString()); - assertEquals("PRP1660", response.get(0).getIdentifier().get(0).getValue().getValue()); + assertEquals("http://foo/Patient?quantityParam=%3E123%7Cfoo%7Cbar", capt.getValue().getURI().toString()); + assertEquals("PRP1660", response.getIdentifier().get(0).getValue().getValue()); } @@ -709,25 +765,6 @@ public class ClientTest { } - @Test - public void testSearchByQuantity() throws Exception { - - String msg = getPatientFeedWithOneResult(); - - ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); - when(httpClient.execute(capt.capture())).thenReturn(httpResponse); - when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); - when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); - when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8"))); - - ITestClient client = ctx.newRestfulClient(ITestClient.class, "http://foo"); - Patient response = client.findPatientQuantity(new QuantityDt(QuantityCompararatorEnum.GREATERTHAN,123L,"foo","bar")); - - assertEquals("http://foo/Patient?quantityParam=%3E123%7Cfoo%7Cbar", capt.getValue().getURI().toString()); - assertEquals("PRP1660", response.getIdentifier().get(0).getValue().getValue()); - - } - @Test public void testSearchComposite() throws Exception { @@ -946,8 +983,7 @@ public class ClientTest { } /** - * Return a FHIR content type, but no content and make sure we handle this - * without crashing + * Return a FHIR content type, but no content and make sure we handle this without crashing */ @Test public void testUpdateWithEmptyResponse() throws Exception { @@ -1063,38 +1099,14 @@ public class ClientTest { } - private String getPatientFeedWithOneResult() { - //@formatter:off - String msg = "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" + - "<title/>\n" + - "<id>d039f91a-cc3c-4013-988e-af4d8d0614bd</id>\n" + - "<os:totalResults xmlns:os=\"http://a9.com/-/spec/opensearch/1.1/\">1</os:totalResults>\n" + - "<published>2014-03-11T16:35:07-04:00</published>\n" + - "<author>\n" + - "<name>ca.uhn.fhir.rest.server.DummyRestfulServer</name>\n" + - "</author>\n" + - "<entry>\n" + - "<content type=\"text/xml\">" - + "<Patient xmlns=\"http://hl7.org/fhir\">" - + "<text><status value=\"generated\" /><div xmlns=\"http://www.w3.org/1999/xhtml\">John Cardinal: 444333333 </div></text>" - + "<identifier><label value=\"SSN\" /><system value=\"http://orionhealth.com/mrn\" /><value value=\"PRP1660\" /></identifier>" - + "<name><use value=\"official\" /><family value=\"Cardinal\" /><given value=\"John\" /></name>" - + "<name><family value=\"Kramer\" /><given value=\"Doe\" /></name>" - + "<telecom><system value=\"phone\" /><value value=\"555-555-2004\" /><use value=\"work\" /></telecom>" - + "<gender><coding><system value=\"http://hl7.org/fhir/v3/AdministrativeGender\" /><code value=\"M\" /></coding></gender>" - + "<address><use value=\"home\" /><line value=\"2222 Home Street\" /></address><active value=\"true\" />" - + "</Patient>" - + "</content>\n" - + " </entry>\n" - + "</feed>"; - //@formatter:on - return msg; - } - private Header[] toHeaderArray(String theName, String theValue) { return new Header[] { new BasicHeader(theName, theValue) }; } + private interface ClientWithoutAnnotation extends IBasicClient { + Patient read(@IdParam IdDt theId); + } + @ResourceDef(name = "Patient") public static class CustomPatient extends Patient { // nothing @@ -1114,8 +1126,4 @@ public class ClientTest { @Search() public Patient getPatientWithIncludes(@RequiredParam(name = "withIncludes") StringDt theString, @IncludeParam String theInclude); } - - private interface ClientWithoutAnnotation extends IBasicClient { - Patient read(@IdParam IdDt theId); - } } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java index b1849b083a4..66b052e6f14 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/GenericClientTest.java @@ -13,6 +13,7 @@ import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.ProtocolVersion; import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicStatusLine; @@ -59,7 +60,7 @@ public class GenericClientTest { } @Test - public void testCreateWithTag() throws Exception { + public void testCreateWithTagNonFluent() throws Exception { Patient p1 = new Patient(); p1.addIdentifier("foo:bar", "12345"); @@ -88,6 +89,52 @@ public class GenericClientTest { assertEquals("urn:happytag; label=\"This is a happy resource\"; scheme=\"http://hl7.org/fhir/tag\"", catH.getValue()); } + + @Test + public void testCreateWithTag() throws Exception { + + Patient p1 = new Patient(); + p1.addIdentifier("foo:bar", "12345"); + p1.addName().addFamily("Smith").addGiven("John"); + TagList list = new TagList(); + list.addTag("http://hl7.org/fhir/tag", "urn:happytag", "This is a happy resource"); + ResourceMetadataKeyEnum.TAG_LIST.put(p1, list); + + ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201, "OK")); + when(myHttpResponse.getAllHeaders()).thenReturn(new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "/Patient/44/_history/22") }); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + + IGenericClient client = myCtx.newRestfulGenericClient("http://example.com/fhir"); + + MethodOutcome outcome = client.create().resource(p1).execute(); + assertEquals("44", outcome.getId().getIdPart()); + assertEquals("22", outcome.getId().getVersionIdPart()); + + assertEquals("http://example.com/fhir/Patient", capt.getValue().getURI().toString()); + assertEquals("POST", capt.getValue().getMethod()); + Header catH = capt.getValue().getFirstHeader("Category"); + assertNotNull(Arrays.asList(capt.getValue().getAllHeaders()).toString(), catH); + assertEquals("urn:happytag; label=\"This is a happy resource\"; scheme=\"http://hl7.org/fhir/tag\"", catH.getValue()); + + /* + * Try fluent options + */ + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + client.create().resource(p1).withId("123").execute(); + assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(1).getURI().toString()); + + String resourceText = "<Patient xmlns=\"http://hl7.org/fhir\"> </Patient>"; + when(myHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(""), Charset.forName("UTF-8"))); + client.create().resource(resourceText).withId("123").execute(); + assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(2).getURI().toString()); + assertEquals(resourceText, IOUtils.toString(((HttpPost)capt.getAllValues().get(2)).getEntity().getContent())); + + } + + private String getPatientFeedWithOneResult() { //@formatter:off String msg = "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" + diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/InterceptorTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/InterceptorTest.java new file mode 100644 index 00000000000..a33f57f190b --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/client/InterceptorTest.java @@ -0,0 +1,83 @@ +package ca.uhn.fhir.rest.client; + +import static org.junit.Assert.assertFalse; + +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.AfterClass; +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.dstu.resource.Patient; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Read; +import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.testutil.RandomServerPortProvider; + +/** + * Created by dsotnikov on 2/25/2014. + */ +public class InterceptorTest { + + private static int ourPort; + private static Server ourServer; + private static FhirContext ourCtx; + + @Test + public void testLogger() throws Exception { + IGenericClient client = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort); + client.registerInterceptor(new LoggingInterceptor(true)); + Patient patient = client.read(Patient.class, "1"); + assertFalse(patient.getIdentifierFirstRep().isEmpty()); + } + + + @AfterClass + public static void afterClass() throws Exception { + ourServer.stop(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourPort = RandomServerPortProvider.findFreePort(); + ourServer = new Server(ourPort); + + DummyProvider patientProvider = new DummyProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(); + ourCtx = servlet.getFhirContext(); + servlet.setResourceProviders(patientProvider); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + } + + /** + * Created by dsotnikov on 2/25/2014. + */ + public static class DummyProvider implements IResourceProvider { + + @Read(version = true) + public Patient findPatient(@IdParam IdDt theId) { + Patient patient = new Patient(); + patient.addIdentifier(theId.getIdPart(), theId.getVersionIdPart()); + patient.setId("Patient/1/_history/1"); + return patient; + } + + @Override + public Class<? extends IResource> getResourceType() { + return Patient.class; + } + + } + +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/CreateTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/CreateTest.java new file mode 100644 index 00000000000..a2e9cd284b7 --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/CreateTest.java @@ -0,0 +1,267 @@ +package ca.uhn.fhir.rest.server; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collection; +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.HttpPost; +import org.apache.http.client.methods.HttpPut; +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.junit.AfterClass; +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.api.ResourceMetadataKeyEnum; +import ca.uhn.fhir.model.api.Tag; +import ca.uhn.fhir.model.api.TagList; +import ca.uhn.fhir.model.dstu.resource.AdverseReaction; +import ca.uhn.fhir.model.dstu.resource.DiagnosticOrder; +import ca.uhn.fhir.model.dstu.resource.DiagnosticReport; +import ca.uhn.fhir.model.dstu.resource.Observation; +import ca.uhn.fhir.model.dstu.resource.OperationOutcome; +import ca.uhn.fhir.model.dstu.resource.Patient; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.annotation.Create; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.annotation.Search; +import ca.uhn.fhir.rest.annotation.Update; +import ca.uhn.fhir.rest.annotation.VersionIdParam; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.testutil.RandomServerPortProvider; + +/** + * Created by dsotnikov on 2/25/2014. + */ +public class CreateTest { + private static CloseableHttpClient ourClient; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CreateTest.class); + private static int ourPort; + private static DiagnosticReportProvider ourReportProvider; + private static Server ourServer; + + + + @Test + public void testCreate() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier().setValue("001"); + patient.addIdentifier().setValue("002"); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient"); + httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + + HttpResponse status = ourClient.execute(httpPost); + + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + + assertEquals(201, status.getStatusLine().getStatusCode()); + assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("location").getValue()); + + } + + + @Test + public void testCreateById() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier().setValue("001"); + patient.addIdentifier().setValue("002"); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient/1234"); + httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + + HttpResponse status = ourClient.execute(httpPost); + + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info("Response was:\n{}", responseContent); + + assertEquals(201, status.getStatusLine().getStatusCode()); + assertEquals("http://localhost:" + ourPort + "/Patient/1234/_history/002", status.getFirstHeader("location").getValue()); + + } + + + + @Test + public void testCreateJson() throws Exception { + + Patient patient = new Patient(); + patient.addIdentifier().setValue("001"); + patient.addIdentifier().setValue("002"); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient"); + httpPost.setEntity(new StringEntity(new FhirContext().newJsonParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_JSON, "UTF-8"))); + + HttpResponse status = ourClient.execute(httpPost); + + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + + ourLog.info("Response was:\n{}", responseContent); + + assertEquals(201, status.getStatusLine().getStatusCode()); + assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("location").getValue()); + + } + + @Test + public void testCreateWithUnprocessableEntity() throws Exception { + + DiagnosticReport report = new DiagnosticReport(); + report.getIdentifier().setValue("001"); + + HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/DiagnosticReport"); + httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(report), ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + + HttpResponse status = ourClient.execute(httpPost); + + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + + ourLog.info("Response was:\n{}", responseContent); + + assertEquals(422, status.getStatusLine().getStatusCode()); + + OperationOutcome outcome = new FhirContext().newXmlParser().parseResource(OperationOutcome.class, new StringReader(responseContent)); + assertEquals("FOOBAR", outcome.getIssueFirstRep().getDetails().getValue()); + + } + + @AfterClass + public static void afterClass() throws Exception { + ourServer.stop(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourPort = RandomServerPortProvider.findFreePort(); + ourServer = new Server(ourPort); + + PatientProvider patientProvider = new PatientProvider(); + + ourReportProvider = new DiagnosticReportProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(); + servlet.setResourceProviders(patientProvider, ourReportProvider, new DummyAdverseReactionResourceProvider()); + ServletHolder servletHolder = new ServletHolder(servlet); + proxyHandler.addServletWithMapping(servletHolder, "/*"); + ourServer.setHandler(proxyHandler); + ourServer.start(); + + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); + HttpClientBuilder builder = HttpClientBuilder.create(); + builder.setConnectionManager(connectionManager); + ourClient = builder.build(); + + } + + public static class DiagnosticReportProvider implements IResourceProvider { + private TagList myLastTags; + private IdDt myLastVersion; + + public TagList getLastTags() { + return myLastTags; + } + + @Override + public Class<? extends IResource> getResourceType() { + return DiagnosticReport.class; + } + + + @Create() + public MethodOutcome createDiagnosticReport(@ResourceParam DiagnosticReport thePatient) { + OperationOutcome outcome = new OperationOutcome(); + outcome.addIssue().setDetails("FOOBAR"); + throw new UnprocessableEntityException(outcome); + } + + } + + /** + * Created by dsotnikov on 2/25/2014. + */ + public static class DummyAdverseReactionResourceProvider implements IResourceProvider { + + /* + * ********************* + * NO NEW METHODS ********************* + */ + + @Create() + public MethodOutcome create(@ResourceParam AdverseReaction thePatient) { + IdDt id = new IdDt(thePatient.getIdentifier().get(0).getValue().getValue()); + IdDt version = new IdDt(thePatient.getIdentifier().get(1).getValue().getValue()); + return new MethodOutcome(id, version); + } + + @Search() + public Collection<AdverseReaction> getAllResources() { + ArrayList<AdverseReaction> retVal = new ArrayList<AdverseReaction>(); + + AdverseReaction ar1 = new AdverseReaction(); + ar1.setId("1"); + retVal.add(ar1); + + AdverseReaction ar2 = new AdverseReaction(); + ar2.setId("2"); + retVal.add(ar2); + + return retVal; + } + + @Override + public Class<? extends IResource> getResourceType() { + return AdverseReaction.class; + } + + } + + + + + + public static class PatientProvider implements IResourceProvider { + + @Override + public Class<? extends IResource> getResourceType() { + return Patient.class; + } + + @Create() + public MethodOutcome createPatient(@ResourceParam Patient thePatient) { + IdDt id = new IdDt(thePatient.getIdentifier().get(0).getValue().getValue()); + if (thePatient.getId().isEmpty()==false) { + id=thePatient.getId(); + } + return new MethodOutcome(id.withVersion("002")); + } + + + } + +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/PagingTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/PagingTest.java index 5b4d4e453fc..3972043fc1c 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/PagingTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/PagingTest.java @@ -52,35 +52,38 @@ public class PagingTest { String link; String base = "http://localhost:" + ourPort; { - HttpGet httpGet = new HttpGet(base+ "/Patient?_format=xml&_pretty=true"); + HttpGet httpGet = new HttpGet(base + "/Patient?_format=xml&_pretty=true"); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); Bundle bundle = ourContext.newXmlParser().parseBundle(responseContent); assertEquals(5, bundle.getEntries().size()); assertEquals("0", bundle.getEntries().get(0).getId().getIdPart()); assertEquals("4", bundle.getEntries().get(4).getId().getIdPart()); - assertEquals(base + '?'+Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=5&" + Constants.PARAM_COUNT + "=5&_format=xml&_pretty=true", bundle.getLinkNext().getValue()); + assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=5&" + Constants.PARAM_COUNT + "=5&_format=xml&_pretty=true", bundle.getLinkNext() + .getValue()); assertNull(bundle.getLinkPrevious().getValue()); - link=bundle.getLinkNext().getValue(); + link = bundle.getLinkNext().getValue(); } { - HttpGet httpGet = new HttpGet(link); + HttpGet httpGet = new HttpGet(link.replace("=xml", "=json")); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); - Bundle bundle = ourContext.newXmlParser().parseBundle(responseContent); + Bundle bundle = ourContext.newJsonParser().parseBundle(responseContent); assertEquals(5, bundle.getEntries().size()); assertEquals("5", bundle.getEntries().get(0).getId().getIdPart()); assertEquals("9", bundle.getEntries().get(4).getId().getIdPart()); assertNull(bundle.getLinkNext().getValue()); - assertEquals(base + '?'+Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=0&" + Constants.PARAM_COUNT + "=5&_format=xml&_pretty=true", bundle.getLinkPrevious().getValue()); + assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=0&" + Constants.PARAM_COUNT + "=5&_format=json&_pretty=true", bundle.getLinkPrevious() + .getValue()); } } - - + @Test public void testSearchInexactOffset() throws Exception { when(myPagingProvider.getDefaultPageSize()).thenReturn(5); @@ -90,9 +93,10 @@ public class PagingTest { String base = "http://localhost:" + ourPort; { - HttpGet httpGet = new HttpGet(base + '?'+Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=8&" + Constants.PARAM_COUNT + "=5&_format=xml&_pretty=true"); + HttpGet httpGet = new HttpGet(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=8&" + Constants.PARAM_COUNT + "=5&_format=xml&_pretty=true"); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); Bundle bundle = ourContext.newXmlParser().parseBundle(responseContent); @@ -100,7 +104,8 @@ public class PagingTest { assertEquals("8", bundle.getEntries().get(0).getId().getIdPart()); assertEquals("9", bundle.getEntries().get(1).getId().getIdPart()); assertNull(bundle.getLinkNext().getValue()); - assertEquals(base + '?'+Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=3&" + Constants.PARAM_COUNT + "=5&_format=xml&_pretty=true", bundle.getLinkPrevious().getValue()); + assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=3&" + Constants.PARAM_COUNT + "=5&_format=xml&_pretty=true", bundle.getLinkPrevious() + .getValue()); } } @@ -115,37 +120,38 @@ public class PagingTest { String link; String base = "http://localhost:" + ourPort; { - HttpGet httpGet = new HttpGet(base+ "/Patient?_count=2"); + HttpGet httpGet = new HttpGet(base + "/Patient?_count=2"); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); Bundle bundle = ourContext.newXmlParser().parseBundle(responseContent); assertEquals(2, bundle.getEntries().size()); assertEquals("0", bundle.getEntries().get(0).getId().getIdPart()); assertEquals("1", bundle.getEntries().get(1).getId().getIdPart()); - assertEquals(base + '?'+Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=2&" + Constants.PARAM_COUNT + "=2&_format=xml", bundle.getLinkNext().getValue()); + assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=2&" + Constants.PARAM_COUNT + "=2&_format=xml", bundle.getLinkNext().getValue()); assertNull(bundle.getLinkPrevious().getValue()); - link=bundle.getLinkNext().getValue(); + link = bundle.getLinkNext().getValue(); } { HttpGet httpGet = new HttpGet(link); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); Bundle bundle = ourContext.newXmlParser().parseBundle(responseContent); assertEquals(2, bundle.getEntries().size()); assertEquals("2", bundle.getEntries().get(0).getId().getIdPart()); assertEquals("3", bundle.getEntries().get(1).getId().getIdPart()); - assertEquals(base + '?'+Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=4&" + Constants.PARAM_COUNT + "=2&_format=xml", bundle.getLinkNext().getValue()); - assertEquals(base + '/'+'?'+Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=2&" + Constants.PARAM_COUNT + "=2&_format=xml", bundle.getLinkSelf().getValue()); - assertEquals(base + '?'+Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=0&" + Constants.PARAM_COUNT + "=2&_format=xml", bundle.getLinkPrevious().getValue()); + assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=4&" + Constants.PARAM_COUNT + "=2&_format=xml", bundle.getLinkNext().getValue()); + assertEquals(base + '/' + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=2&" + Constants.PARAM_COUNT + "=2&_format=xml", bundle.getLinkSelf().getValue()); + assertEquals(base + '?' + Constants.PARAM_PAGINGACTION + "=ABCD&" + Constants.PARAM_PAGINGOFFSET + "=0&" + Constants.PARAM_COUNT + "=2&_format=xml", bundle.getLinkPrevious().getValue()); } } - @AfterClass public static void afterClass() throws Exception { ourServer.stop(); diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java index c723bfb50d0..61052f2920f 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ResfulServerMethodTest.java @@ -109,71 +109,6 @@ public class ResfulServerMethodTest { assertThat(responseContent, StringContains.containsString("AAAABBBB")); } - @Test - public void testCreate() throws Exception { - - Patient patient = new Patient(); - patient.addIdentifier().setValue("001"); - patient.addIdentifier().setValue("002"); - - HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient"); - httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); - - HttpResponse status = ourClient.execute(httpPost); - - String responseContent = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); - ourLog.info("Response was:\n{}", responseContent); - - assertEquals(201, status.getStatusLine().getStatusCode()); - assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("location").getValue()); - - } - - @Test - public void testCreateJson() throws Exception { - - Patient patient = new Patient(); - patient.addIdentifier().setValue("001"); - patient.addIdentifier().setValue("002"); - - HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient"); - httpPost.setEntity(new StringEntity(new FhirContext().newJsonParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_JSON, "UTF-8"))); - - HttpResponse status = ourClient.execute(httpPost); - - String responseContent = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); - - ourLog.info("Response was:\n{}", responseContent); - - assertEquals(201, status.getStatusLine().getStatusCode()); - assertEquals("http://localhost:" + ourPort + "/Patient/001/_history/002", status.getFirstHeader("location").getValue()); - - } - - @Test - public void testCreateWithUnprocessableEntity() throws Exception { - - DiagnosticReport report = new DiagnosticReport(); - report.getIdentifier().setValue("001"); - - HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/DiagnosticReport"); - httpPost.setEntity(new StringEntity(new FhirContext().newXmlParser().encodeResourceToString(report), ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); - - HttpResponse status = ourClient.execute(httpPost); - - String responseContent = IOUtils.toString(status.getEntity().getContent()); - IOUtils.closeQuietly(status.getEntity().getContent()); - - ourLog.info("Response was:\n{}", responseContent); - - assertEquals(422, status.getStatusLine().getStatusCode()); - - OperationOutcome outcome = new FhirContext().newXmlParser().parseResource(OperationOutcome.class, new StringReader(responseContent)); - assertEquals("FOOBAR", outcome.getIssueFirstRep().getDetails().getValue()); - - } @Test public void testDateRangeParam() throws Exception { @@ -1102,18 +1037,6 @@ public class ResfulServerMethodTest { */ public static class DummyAdverseReactionResourceProvider implements IResourceProvider { - /* - * ********************* - * NO NEW METHODS ********************* - */ - - @Create() - public MethodOutcome create(@ResourceParam AdverseReaction thePatient) { - IdDt id = new IdDt(thePatient.getIdentifier().get(0).getValue().getValue()); - IdDt version = new IdDt(thePatient.getIdentifier().get(1).getValue().getValue()); - return new MethodOutcome(id, version); - } - @Search() public Collection<AdverseReaction> getAllResources() { ArrayList<AdverseReaction> retVal = new ArrayList<AdverseReaction>(); @@ -1148,12 +1071,6 @@ public class ResfulServerMethodTest { throw new ResourceNotFoundException("AAAABBBB"); } - @Create() - public MethodOutcome createDiagnosticReport(@ResourceParam DiagnosticReport thePatient) { - OperationOutcome outcome = new OperationOutcome(); - outcome.addIssue().setDetails("FOOBAR"); - throw new UnprocessableEntityException(outcome); - } @Delete() public void deleteDiagnosticReport(@IdParam IdDt theId) { @@ -1176,12 +1093,6 @@ public class ResfulServerMethodTest { */ public static class DummyPatientResourceProvider implements IResourceProvider { - @Create() - public MethodOutcome createPatient(@ResourceParam Patient thePatient) { - IdDt id = new IdDt(thePatient.getIdentifier().get(0).getValue().getValue()); - IdDt version = new IdDt(thePatient.getIdentifier().get(1).getValue().getValue()); - return new MethodOutcome(id, version); - } @Delete() public MethodOutcome deletePatient(@IdParam IdDt theId) { diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesTest.java index dd7d0e8cddc..e3515ad714c 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ServerFeaturesTest.java @@ -10,9 +10,11 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import org.apache.commons.io.IOUtils; +import org.apache.http.Header; 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.HttpOptions; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; @@ -114,6 +116,39 @@ public class ServerFeaturesTest { } + + @Test + public void testCors() throws Exception { + servlet.setCorsAllowDomain("http://foo.com"); + + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1"); + httpGet.addHeader("Accept", Constants.CT_FHIR_XML); + HttpResponse status = ourClient.execute(httpGet); + IOUtils.closeQuietly(status.getEntity().getContent()); + + Header origin = status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN); + assertEquals("http://foo.com", origin.getValue()); + } + + + @Test + public void testOptions() throws Exception { + servlet.setCorsAllowDomain("http://foo.com"); + + HttpOptions httpGet = new HttpOptions("http://localhost:" + ourPort + "/"); + httpGet.addHeader("Accept", Constants.CT_FHIR_XML); + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + + Header origin = status.getFirstHeader(Constants.HEADER_CORS_ALLOW_ORIGIN); + assertEquals("http://foo.com", origin.getValue()); + + assertThat(responseContent,StringContains.containsString("<Conformance")); + + } + + @Test public void testAcceptHeaderWithMultiple() throws Exception { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1"); @@ -235,8 +270,10 @@ public class ServerFeaturesTest { @Before public void before() { servlet.setServerAddressStrategy(new IncomingRequestAddressStrategy()); + servlet.setCorsAllowDomain(null); } + @BeforeClass public static void beforeClass() throws Exception { ourPort = RandomServerPortProvider.findFreePort(); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java index af8523dd115..d2d589afa5e 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java @@ -1053,6 +1053,11 @@ public abstract class BaseFhirDao implements IDao { if (entity.getId() == null) { myEntityManager.persist(entity); + + if (entity.getForcedId() != null) { + myEntityManager.persist(entity.getForcedId()); + } + } else { entity = myEntityManager.merge(entity); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDao.java index 118470602fe..cc6eaa7af3b 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDao.java @@ -24,6 +24,7 @@ import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Expression; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; +import javax.validation.ConstraintViolationException; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -42,6 +43,7 @@ import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.context.RuntimeSearchParam; import ca.uhn.fhir.jpa.entity.BaseHasResource; import ca.uhn.fhir.jpa.entity.BaseTag; +import ca.uhn.fhir.jpa.entity.ForcedId; import ca.uhn.fhir.jpa.entity.ResourceHistoryTable; import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamDate; import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamNumber; @@ -403,7 +405,8 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements } if (rawSearchTerm.length() > ResourceIndexedSearchParamString.MAX_LENGTH) { - throw new InvalidRequestException("Parameter[" + theParamName + "] has length (" + rawSearchTerm.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm); + throw new InvalidRequestException("Parameter[" + theParamName + "] has length (" + rawSearchTerm.length() + ") that is longer than maximum allowed (" + + ResourceIndexedSearchParamString.MAX_LENGTH + "): " + rawSearchTerm); } String likeExpression = normalizeString(rawSearchTerm); @@ -461,10 +464,12 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements } if (system != null && system.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) { - throw new InvalidRequestException("Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system); + throw new InvalidRequestException("Parameter[" + theParamName + "] has system (" + system.length() + ") that is longer than maximum allowed (" + + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + system); } if (code != null && code.length() > ResourceIndexedSearchParamToken.MAX_LENGTH) { - throw new InvalidRequestException("Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + "): " + code); + throw new InvalidRequestException("Parameter[" + theParamName + "] has code (" + code.length() + ") that is longer than maximum allowed (" + ResourceIndexedSearchParamToken.MAX_LENGTH + + "): " + code); } ArrayList<Predicate> singleCodePredicates = (new ArrayList<Predicate>()); @@ -533,9 +538,20 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements if (theResource.getId().isEmpty() == false) { if (isValidPid(theResource.getId())) { - throw new UnprocessableEntityException("This server cannot create an entity with a numeric ID - Numeric IDs are server assigned"); + throw new UnprocessableEntityException( + "This server cannot create an entity with a user-specified numeric ID - Client should not specify an ID when creating a new resource, or should include at least one letter in the ID to force a client-defined ID"); } createForcedIdIfNeeded(entity, theResource.getId()); + + if (entity.getForcedId() != null) { + try { + translateForcedIdToPid(theResource.getId()); + throw new UnprocessableEntityException("Can not create entity with ID[" + theResource.getId().getValue() + "], constraint violation occurred"); + } catch (ResourceNotFoundException e) { + // good, this ID doesn't exist so we can create it + } + } + } updateEntity(theResource, entity, false, false); @@ -608,7 +624,8 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements final T current = currentTmp; - String querySring = "SELECT count(h) FROM ResourceHistoryTable h " + "WHERE h.myResourceId = :PID AND h.myResourceType = :RESTYPE" + " AND h.myUpdated < :END" + (theSince != null ? " AND h.myUpdated >= :SINCE" : ""); + String querySring = "SELECT count(h) FROM ResourceHistoryTable h " + "WHERE h.myResourceId = :PID AND h.myResourceType = :RESTYPE" + " AND h.myUpdated < :END" + + (theSince != null ? " AND h.myUpdated >= :SINCE" : ""); TypedQuery<Long> countQuery = myEntityManager.createQuery(querySring, Long.class); countQuery.setParameter("PID", theId.getIdPartAsLong()); countQuery.setParameter("RESTYPE", resourceType); @@ -646,8 +663,9 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements retVal.add(current); } - TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery("SELECT h FROM ResourceHistoryTable h WHERE h.myResourceId = :PID AND h.myResourceType = :RESTYPE AND h.myUpdated < :END " + (theSince != null ? " AND h.myUpdated >= :SINCE" : "") - + " ORDER BY h.myUpdated ASC", ResourceHistoryTable.class); + TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery( + "SELECT h FROM ResourceHistoryTable h WHERE h.myResourceId = :PID AND h.myResourceType = :RESTYPE AND h.myUpdated < :END " + + (theSince != null ? " AND h.myUpdated >= :SINCE" : "") + " ORDER BY h.myUpdated ASC", ResourceHistoryTable.class); q.setParameter("PID", theId.getIdPartAsLong()); q.setParameter("RESTYPE", resourceType); q.setParameter("END", end.getValue(), TemporalType.TIMESTAMP); @@ -716,7 +734,8 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements throw new ConfigurationException("Unknown search param on resource[" + myResourceName + "] for secondary key[" + mySecondaryPrimaryKeyParamName + "]"); } if (sp.getParamType() != SearchParamTypeEnum.TOKEN) { - throw new ConfigurationException("Search param on resource[" + myResourceName + "] for secondary key[" + mySecondaryPrimaryKeyParamName + "] is not a token type, only token is supported"); + throw new ConfigurationException("Search param on resource[" + myResourceName + "] for secondary key[" + mySecondaryPrimaryKeyParamName + + "] is not a token type, only token is supported"); } } @@ -743,7 +762,8 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements private void validateResourceType(BaseHasResource entity) { if (!myResourceName.equals(entity.getResourceType())) { - throw new ResourceNotFoundException("Resource with ID " + entity.getIdDt().getIdPart() + " exists but it is not of type " + myResourceName + ", found resource of type " + entity.getResourceType()); + throw new ResourceNotFoundException("Resource with ID " + entity.getIdDt().getIdPart() + " exists but it is not of type " + myResourceName + ", found resource of type " + + entity.getResourceType()); } } @@ -767,7 +787,8 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements if (entity == null) { if (theId.hasVersionIdPart()) { - TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery("SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class); + TypedQuery<ResourceHistoryTable> q = myEntityManager.createQuery( + "SELECT t from ResourceHistoryTable t WHERE t.myResourceId = :RID AND t.myResourceType = :RTYP AND t.myResourceVersion = :RVER", ResourceHistoryTable.class); q.setParameter("RID", theId.getIdPartAsLong()); q.setParameter("RTYP", myResourceName); q.setParameter("RVER", theId.getVersionIdPartAsLong()); @@ -1048,8 +1069,7 @@ public class FhirResourceDao<T extends IResource> extends BaseFhirDao implements } /** - * If set, the given param will be treated as a secondary primary key, and multiple resources will not be able to - * share the same value. + * If set, the given param will be treated as a secondary primary key, and multiple resources will not be able to share the same value. */ public void setSecondaryPrimaryKeyParamName(String theSecondaryPrimaryKeyParamName) { mySecondaryPrimaryKeyParamName = theSecondaryPrimaryKeyParamName; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java index 2f9080f322d..1dfccd68896 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java @@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.entity; import java.util.Collection; import java.util.Date; +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.EnumType; import javax.persistence.Enumerated; @@ -14,6 +15,8 @@ import javax.persistence.OneToOne; import javax.persistence.Temporal; import javax.persistence.TemporalType; +import org.hibernate.validator.cfg.context.Cascadable; + import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; diff --git a/hapi-fhir-jpaserver-test/src/test/java/ca/uhn/fhir/jpa/test/CompleteResourceProviderTest.java b/hapi-fhir-jpaserver-test/src/test/java/ca/uhn/fhir/jpa/test/CompleteResourceProviderTest.java index 9361291a442..b74763bb251 100644 --- a/hapi-fhir-jpaserver-test/src/test/java/ca/uhn/fhir/jpa/test/CompleteResourceProviderTest.java +++ b/hapi-fhir-jpaserver-test/src/test/java/ca/uhn/fhir/jpa/test/CompleteResourceProviderTest.java @@ -18,8 +18,10 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.provider.JpaConformanceProvider; +import ca.uhn.fhir.jpa.provider.JpaSystemProvider; import ca.uhn.fhir.jpa.testutil.RandomServerPortProvider; import ca.uhn.fhir.model.api.Bundle; +import ca.uhn.fhir.model.api.BundleEntry; import ca.uhn.fhir.model.api.ExtensionDt; import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt; import ca.uhn.fhir.model.dstu.resource.Conformance; @@ -36,6 +38,7 @@ import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.rest.client.IGenericClient; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; import ca.uhn.fhir.util.ExtensionConstants; import ca.uhn.test.jpasrv.ObservationResourceProvider; import ca.uhn.test.jpasrv.OrganizationResourceProvider; @@ -51,7 +54,8 @@ public class CompleteResourceProviderTest { private static IFhirResourceDao<Questionnaire> questionnaireDao; private static IGenericClient ourClient; private static IFhirResourceDao<Observation> observationDao; -// private static JpaConformanceProvider ourConfProvider; + + // private static JpaConformanceProvider ourConfProvider; // @Test // public void test01UploadTestResources() throws Exception { @@ -92,20 +96,46 @@ public class CompleteResourceProviderTest { } - + @Test + public void testCreateWithId() { + Patient p1 = new Patient(); + p1.addIdentifier().setSystem("urn:system").setValue("testCreateWithId01"); + IdDt p1Id = ourClient.create().resource(p1).withId("testCreateWithId").execute().getId(); + + assertThat(p1Id.getValue(), containsString("Patient/testCreateWithId/_history")); + + Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode("urn:system", "testCreateWithId01")).encodedJson().prettyPrint().execute(); + assertEquals(1, actual.size()); + assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getId().getIdPart()); + + /* + * ensure that trying to create the same ID again fails appropriately + */ + try { + ourClient.create().resource(p1).withId("testCreateWithId").execute().getId(); + fail(); + } catch (UnprocessableEntityException e) { + // good + } + + Bundle history = ourClient.history(null, (String)null, null, null); + assertEquals(p1Id.getIdPart(), history.getEntries().get(0).getId().getIdPart()); + assertNotNull(history.getEntries().get(0).getResource()); + } + @Test public void testSearchByIdentifierWithoutSystem() { Patient p1 = new Patient(); p1.addIdentifier().setValue("testSearchByIdentifierWithoutSystem01"); IdDt p1Id = ourClient.create(p1).getId(); - Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode(null, "testSearchByIdentifierWithoutSystem01")).encodedJson().prettyPrint().execute(); + Bundle actual = ourClient.search().forResource(Patient.class).where(Patient.IDENTIFIER.exactly().systemAndCode(null, "testSearchByIdentifierWithoutSystem01")).encodedJson().prettyPrint() + .execute(); assertEquals(1, actual.size()); assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getId().getIdPart()); } - @Test public void testSearchByResourceChain() { Organization o1 = new Organization(); @@ -137,48 +167,48 @@ public class CompleteResourceProviderTest { assertEquals(p1Id.getIdPart(), actual.getEntries().get(0).getId().getIdPart()); } -private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CompleteResourceProviderTest.class); + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(CompleteResourceProviderTest.class); + @Test public void testInsertUpdatesConformance() { -// Conformance conf = ourConfProvider.getServerConformance(); -// ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conf)); -// -// RestResource res=null; -// for (Rest nextRest : conf.getRest()) { -// for (RestResource nextRes : nextRest.getResource()) { -// if (nextRes.getType().getValueAsEnum()==ResourceTypeEnum.PATIENT) { -// res = nextRes; -// } -// } -// } -// List<ExtensionDt> resCounts = res.getUndeclaredExtensionsByUrl(ExtensionConstants.CONF_RESOURCE_COUNT); -// -// int initial = 0; - + // Conformance conf = ourConfProvider.getServerConformance(); + // ourLog.info(ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conf)); + // + // RestResource res=null; + // for (Rest nextRest : conf.getRest()) { + // for (RestResource nextRes : nextRest.getResource()) { + // if (nextRes.getType().getValueAsEnum()==ResourceTypeEnum.PATIENT) { + // res = nextRes; + // } + // } + // } + // List<ExtensionDt> resCounts = res.getUndeclaredExtensionsByUrl(ExtensionConstants.CONF_RESOURCE_COUNT); + // + // int initial = 0; + Patient p1 = new Patient(); p1.addIdentifier().setSystem("urn:system").setValue("testSearchByResourceChain01"); p1.addName().addFamily("testSearchByResourceChainFamily01").addGiven("testSearchByResourceChainGiven01"); ourClient.create(p1).getId(); -// conf = ourConfProvider.getServerConformance(); -// res=null; -// for (Rest nextRest : conf.getRest()) { -// for (RestResource nextRes : nextRest.getResource()) { -// if (nextRes.getType().getValueAsEnum()==ResourceTypeEnum.PATIENT) { -// res = nextRes; -// } -// } -// } -// resCounts = res.getUndeclaredExtensionsByUrl(ExtensionConstants.CONF_RESOURCE_COUNT); -// assertNotNull(resCounts); -// assertEquals(1, resCounts.size()); -// DecimalDt number = (DecimalDt) resCounts.get(0).getValue(); -// assertEquals(initial+1, number.getValueAsInteger()); + // conf = ourConfProvider.getServerConformance(); + // res=null; + // for (Rest nextRest : conf.getRest()) { + // for (RestResource nextRes : nextRest.getResource()) { + // if (nextRes.getType().getValueAsEnum()==ResourceTypeEnum.PATIENT) { + // res = nextRes; + // } + // } + // } + // resCounts = res.getUndeclaredExtensionsByUrl(ExtensionConstants.CONF_RESOURCE_COUNT); + // assertNotNull(resCounts); + // assertEquals(1, resCounts.size()); + // DecimalDt number = (DecimalDt) resCounts.get(0).getValue(); + // assertEquals(initial+1, number.getValueAsInteger()); } - - @Test public void testInsertBadReference() { Patient p1 = new Patient(); @@ -195,7 +225,6 @@ private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger } - @Test public void testSaveAndRetrieveExistingNarrative() { Patient p1 = new Patient(); @@ -218,7 +247,7 @@ private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger Patient actual = ourClient.read(Patient.class, newId); assertThat(actual.getText().getDiv().getValueAsString(), containsString("<td>Identifier</td><td>testSearchByResourceChain01</td>")); } - + @AfterClass public static void afterClass() throws Exception { ourServer.stop(); @@ -251,9 +280,11 @@ private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger restServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator()); IFhirSystemDao systemDao = (IFhirSystemDao) ourAppCtx.getBean("mySystemDao", IFhirSystemDao.class); - -// ourConfProvider = new JpaConformanceProvider(restServer, systemDao, Collections.singletonList((IFhirResourceDao)patientDao)); + JpaSystemProvider systemProv = new JpaSystemProvider(systemDao); + restServer.setPlainProviders(systemProv); + // ourConfProvider = new JpaConformanceProvider(restServer, systemDao, Collections.singletonList((IFhirResourceDao)patientDao)); + int myPort = RandomServerPortProvider.findFreePort(); ourServer = new Server(myPort); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component b/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component index a513f5f6720..5e7d71c6700 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component +++ b/hapi-fhir-jpaserver-uhnfhirtest/.settings/org.eclipse.wst.common.component @@ -6,7 +6,13 @@ <wb-resource deploy-path="/WEB-INF/classes" source-path="/src/main/resources"/> <wb-resource deploy-path="/" source-path="/target/m2e-wtp/web-resources"/> <wb-resource deploy-path="/" source-path="/src/main/webapp" tag="defaultRootSource"/> - <dependent-module deploy-path="/" handle="module:/overlay/prj/hapi-fhir-tester-overlay?includes=**/**&excludes=META-INF/MANIFEST.MF"> + <dependent-module archiveName="hapi-fhir-jpaserver-base-0.5-SNAPSHOT.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/hapi-fhir-jpaserver-base/hapi-fhir-jpaserver-base"> + <dependency-type>uses</dependency-type> + </dependent-module> + <dependent-module archiveName="hapi-fhir-base-0.5-SNAPSHOT.jar" deploy-path="/WEB-INF/lib" handle="module:/resource/hapi-fhir-base/hapi-fhir-base"> + <dependency-type>uses</dependency-type> + </dependent-module> + <dependent-module deploy-path="/" handle="module:/overlay/prj/hapi-fhir-testpage-overlay?includes=**/**&excludes=META-INF/MANIFEST.MF"> <dependency-type>consumes</dependency-type> </dependent-module> <dependent-module deploy-path="/" handle="module:/overlay/slf/?includes=**/**&excludes=META-INF/MANIFEST.MF"> diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 6bc819d935e..3f71e37d6b5 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -77,7 +77,12 @@ <artifactId>jcl-over-slf4j</artifactId> <version>${slf4j_version}</version> </dependency> - + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>${slf4j_version}</version> + </dependency> + <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java index 84ebe1b2951..5dae69ec9a3 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java @@ -9,7 +9,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.web.context.ContextLoaderListener; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.jpa.dao.IFhirResourceDao; import ca.uhn.fhir.jpa.dao.IFhirSystemDao; import ca.uhn.fhir.jpa.provider.JpaConformanceProvider; import ca.uhn.fhir.jpa.provider.JpaSystemProvider; @@ -69,6 +68,7 @@ public class TestRestfulServer extends RestfulServer { setServerConformanceProvider(confProvider); setUseBrowserFriendlyContentTypes(true); + setCorsAllowDomain("*"); String baseUrl = System.getProperty("fhir.baseurl"); if (StringUtils.isBlank(baseUrl)) { diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/test/java/ca/uhn/fhirtest/PopulateProfiles.java b/hapi-fhir-jpaserver-uhnfhirtest/src/test/java/ca/uhn/fhirtest/PopulateProfiles.java new file mode 100644 index 00000000000..9a40a2d2d63 --- /dev/null +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/test/java/ca/uhn/fhirtest/PopulateProfiles.java @@ -0,0 +1,26 @@ +package ca.uhn.fhirtest; + +import ca.uhn.fhir.model.dstu.resource.Profile; +import ca.uhn.fhir.model.dstu.resource.Profile.ExtensionDefn; +import ca.uhn.fhir.model.dstu.valueset.DataTypeEnum; +import ca.uhn.fhir.model.dstu.valueset.ExtensionContextEnum; + +public class PopulateProfiles { + + public static void main(String[] args) { + + Profile hapiExtensions = new Profile(); + + ExtensionDefn ext = hapiExtensions.addExtensionDefn(); + ext.addContext("Conformance.rest.resource"); + ext.getCode().setValue("resourceCount"); + ext.getContextType().setValueAsEnum(ExtensionContextEnum.RESOURCE); + ext.getDisplay().setValue("Resource count on server"); + ext.getDefinition().addType().setCode(DataTypeEnum.DECIMAL); + + + + + } + +} diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java index 4e231888cf0..b52a76cb118 100644 --- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java @@ -53,6 +53,7 @@ import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.rest.client.GenericClient; import ca.uhn.fhir.rest.client.IGenericClient; +import ca.uhn.fhir.rest.gclient.ICreateTyped; import ca.uhn.fhir.rest.gclient.IQuery; import ca.uhn.fhir.rest.gclient.IUntypedQuery; import ca.uhn.fhir.rest.gclient.StringParam; @@ -91,10 +92,6 @@ public class Controller { return "about"; } - private String logPrefix(ModelMap theModel) { - return "[server=" + theModel.get("serverId") + "] - "; - } - @RequestMapping(value = { "/conformance" }) public String actionConformance(final HomeRequest theRequest, final BindingResult theBindingResult, final ModelMap theModel) { addCommonParams(theRequest, theModel); @@ -160,18 +157,6 @@ public class Controller { return "result"; } - private ResultType handleClientException(GenericClient theClient, Exception e, ModelMap theModel) { - ResultType returnsResource; - returnsResource = ResultType.NONE; - ourLog.warn("Failed to invoke server", e); - - if (theClient.getLastResponse() == null) { - theModel.put("errorMsg", "Error: " + e.getMessage()); - } - - return returnsResource; - } - @RequestMapping(value = { "/get-tags" }) public String actionGetTags(HttpServletRequest theReq, HomeRequest theRequest, BindingResult theBindingResult, ModelMap theModel) { addCommonParams(theRequest, theModel); @@ -527,6 +512,12 @@ public class Controller { return "result"; } + @RequestMapping(value = { "/update" }) + public String actionUpdate(final HttpServletRequest theReq, final HomeRequest theRequest, final BindingResult theBindingResult, final ModelMap theModel) { + doActionCreateOrValidate(theReq, theRequest, theBindingResult, theModel, "update"); + return "result"; + } + @RequestMapping(value = { "/validate" }) public String actionValidate(final HttpServletRequest theReq, final HomeRequest theRequest, final BindingResult theBindingResult, final ModelMap theModel) { doActionCreateOrValidate(theReq, theRequest, theBindingResult, theModel, "validate"); @@ -614,13 +605,17 @@ public class Controller { client.validate(resource); } else { String id = theReq.getParameter("resource-create-id"); - if (isNotBlank(id)) { + if ("update".equals(theMethod)) { outcomeDescription = "Update Resource"; client.update(id, resource); update = true; } else { outcomeDescription = "Create Resource"; - client.create(resource); + ICreateTyped create = client.create().resource(body); + if (isNotBlank(id)) { + create.withId(id); + } + create.execute(); } } } catch (Exception e) { @@ -802,19 +797,19 @@ public class Controller { } try { - str=URLDecoder.decode(str, "UTF-8"); + str = URLDecoder.decode(str, "UTF-8"); } catch (UnsupportedEncodingException e) { - ourLog.error("Should not happen",e); + ourLog.error("Should not happen", e); } - + StringBuilder b = new StringBuilder(); b.append("<span class='hlUrlBase'>"); boolean inParams = false; for (int i = 0; i < str.length(); i++) { char nextChar = str.charAt(i); -// char nextChar2 = i < str.length()-2 ? str.charAt(i+1):' '; -// char nextChar3 = i < str.length()-2 ? str.charAt(i+2):' '; + // char nextChar2 = i < str.length()-2 ? str.charAt(i+1):' '; + // char nextChar3 = i < str.length()-2 ? str.charAt(i+2):' '; if (!inParams) { if (nextChar == '?') { inParams = true; @@ -830,8 +825,8 @@ public class Controller { b.append("</span><wbr /><span class='hlControl'>&</span><span class='hlTagName'>"); } else if (nextChar == '=') { b.append("</span><span class='hlControl'>=</span><span class='hlAttr'>"); -// }else if (nextChar=='%' && Character.isLetterOrDigit(nextChar2)&& Character.isLetterOrDigit(nextChar3)) { -// URLDecoder.decode(s, enc) + // }else if (nextChar=='%' && Character.isLetterOrDigit(nextChar2)&& Character.isLetterOrDigit(nextChar3)) { + // URLDecoder.decode(s, enc) } else { b.append(nextChar); } @@ -853,6 +848,18 @@ public class Controller { return def; } + private ResultType handleClientException(GenericClient theClient, Exception e, ModelMap theModel) { + ResultType returnsResource; + returnsResource = ResultType.NONE; + ourLog.warn("Failed to invoke server", e); + + if (theClient.getLastResponse() == null) { + theModel.put("errorMsg", "Error: " + e.getMessage()); + } + + return returnsResource; + } + private boolean handleSearchParam(String paramIdxString, HttpServletRequest theReq, IQuery theQuery, JsonGenerator theClientCodeJsonWriter) { String nextName = theReq.getParameter("param." + paramIdxString + ".name"); if (isBlank(nextName)) { @@ -970,6 +977,10 @@ public class Controller { return conformance; } + private String logPrefix(ModelMap theModel) { + return "[server=" + theModel.get("serverId") + "] - "; + } + private String parseNarrative(EncodingEnum theCtEnum, String theResultBody) { try { IResource resource = theCtEnum.newParser(myCtx).parseResource(theResultBody); diff --git a/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/resource.html b/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/resource.html index 221ab06c529..462bfa67d11 100644 --- a/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/resource.html +++ b/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/resource.html @@ -337,12 +337,11 @@ </div> <br clear="all"/> - <!-- Create/Update --> + <!-- Create --> <div class="row-fluid"> - <b>Create/Update</b> an instance of the resource. If no ID is specified, - a new resource will be created. If an ID is specified, the existing - resource with that ID will be updated. + <b>Create</b> an instance of the resource. Generally you do not need to specify an ID + but you may force the server to use a specific ID by including one. </div> <div class="row-fluid top-buffer"> <div class="col-sm-2"> @@ -358,7 +357,7 @@ <div class="input-group-addon"> ID </div> - <input type="text" class="form-control" id="resource-create-id" placeholder="(add for update)" th:value="${updateResourceId}"/> + <input type="text" class="form-control" id="resource-create-id" placeholder="(optional)"/> </div> </div> </div> @@ -370,37 +369,16 @@ <span class="loadingStar">*</span> </div> <textarea class="form-control" id="resource-create-body" style="white-space: nowrap; overflow: auto;" placeholder="(place resource body here)" rows="1"> - <th:block th:if="${updateResource} != null" th:text="${updateResource}"/> </textarea> </div> </div> </div> <script type="text/javascript"> - var buttonChanger = function() { - var val = $('#resource-create-id').val(); - if (val != "") { - //$('#resource-create-btn').text("Update"); - $("#resource-create-btn").fadeOut(function() { - $(this).html('<i class="fa fa-pencil"></i> Update').fadeIn(); - }); - } else { - $("#resource-create-btn").fadeOut(function() { - $(this).html('<i class="fa fa-send"></i> Create').fadeIn(); - }); - //$('#resource-create-btn').text("Create"); - } - }; var textAreaChanger = function() { createBodyOriginalHeight = $('#resource-create-body').height(); $('#resource-create-body').animate({height: "200px"}, 500); } - $('#resource-create-id').change(buttonChanger); - $('#resource-create-id').keyup(buttonChanger); $('#resource-create-body').focus(textAreaChanger); - /*$('#resource-create-body').blur( - function() { - $('#resource-create-body').animate({height: "34px"}, 500); - });*/ $('#resource-create-btn').click( function() { var btn = $(this); @@ -422,6 +400,72 @@ </div> <br clear="all"/> + <!-- Update --> + + <div class="row-fluid"> + <b>Update</b> an existing instance of the resource by ID. + </div> + <div class="row-fluid top-buffer"> + <div class="col-sm-2"> + <button type="button" id="resource-update-btn" + data-loading-text="Loading <i class='fa fa-spinner fa-spin'/>" class="btn btn-primary btn-block"> + <i class="fa fa-send"></i> + Update + </button> + </div> + <div class='col-sm-3'> + <div class="form-group"> + <div class='input-group date'> + <div class="input-group-addon"> + ID + <span class="loadingStar">*</span> + </div> + <input type="text" class="form-control" id="resource-update-id" placeholder="(resource ID)" th:value="${updateResourceId}"/> + </div> + </div> + </div> + <div class='col-sm-7'> + <div class="form-group"> + <div class='input-group date'> + <div class="input-group-addon"> + Contents + <span class="loadingStar">*</span> + </div> + <textarea class="form-control" id="resource-update-body" style="white-space: nowrap; overflow: auto;" placeholder="(place resource body here)" rows="1"> + <th:block th:if="${updateResource} != null" th:text="${updateResource}"/> + </textarea> + </div> + </div> + </div> + <script type="text/javascript"> + var textAreaChanger = function() { + updateBodyOriginalHeight = $('#resource-update-body').height(); + $('#resource-update-body').animate({height: "200px"}, 500); + } + $('#resource-update-body').focus(textAreaChanger); + $('#resource-update-btn').click( + function() { + var btn = $(this); + btn.button('loading'); + var id = $('#resource-update-id').val(); + // Note we're using resource-create-id even though this is an update because + // the controller expects that... + if (id != null) btn.append($('<input />', { type: 'hidden', name: 'resource-create-id', value: id })); + var body = $('#resource-update-body').val(); + btn.append($('<input />', { type: 'hidden', name: 'resource-create-body', value: body })); + $("#outerForm").attr("action", "update").submit(); + }); + $( document ).ready(function() { + if ($('#resource-update-id').val() != "") { + buttonChanger(); + textAreaChanger(); + $('#resource-update-body').focus(); + } + }); + </script> + </div> + <br clear="all"/> + <!-- Validate --> <div class="row-fluid"> diff --git a/pom.xml b/pom.xml index b58346b0ff1..e00cb8817a0 100644 --- a/pom.xml +++ b/pom.xml @@ -81,7 +81,7 @@ <maven_site_plugin_version>3.3</maven_site_plugin_version> <mockito_version>1.9.5</mockito_version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> - <slf4j_version>1.7.2</slf4j_version> + <slf4j_version>1.7.7</slf4j_version> <spring_version>4.0.1.RELEASE</spring_version> <thymeleaf-version>2.1.3.RELEASE</thymeleaf-version> </properties>