diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/BaseClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/BaseClient.java index f5fbeb96efeb..a7b20c73c5de 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/BaseClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/BaseClient.java @@ -153,10 +153,14 @@ T invokeClient(FhirContext theContext, IClientResponseHandler binding, Ba return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse); } + void forceConformanceCheck() { + myFactory.validateServerBase(myUrlBase, myClient, this); + } + T invokeClient(FhirContext theContext, IClientResponseHandler binding, BaseHttpClientInvocation clientInvocation, EncodingEnum theEncoding, Boolean thePrettyPrint, boolean theLogRequestAndResponse) { if (!myDontValidateConformance) { - myFactory.validateServerBaseIfConfiguredToDoSo(myUrlBase, myClient); + myFactory.validateServerBaseIfConfiguredToDoSo(myUrlBase, myClient, this); } // TODO: handle non 2xx status codes by throwing the correct exception, @@ -441,4 +445,8 @@ public static Reader createReaderFromResponse(HttpResponse theResponse) throws I return reader; } + public List getInterceptors() { + return Collections.unmodifiableList(myInterceptors); + } + } 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 8564bf3834d8..8e898b996593 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 @@ -140,7 +140,7 @@ public GenericClient(FhirContext theContext, HttpClient theHttpClient, String th super(theHttpClient, theServerBase, theFactory); myContext = theContext; } - + @Override public BaseConformance conformance() { HttpGetClientInvocation invocation = MethodUtil.createConformanceInvocation(); @@ -156,6 +156,11 @@ public BaseConformance conformance() { return resp; } + @Override + public void forceConformanceCheck() { + super.forceConformanceCheck(); + } + @Override public ICreate create() { return new CreateInternal(); 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 eabef4c4ed0d..ad1de980c03e 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 @@ -34,6 +34,8 @@ import ca.uhn.fhir.model.primitive.UriDt; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.api.IRestfulClient; +import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; +import ca.uhn.fhir.rest.client.exceptions.FhirClientInnapropriateForServerException; import ca.uhn.fhir.rest.gclient.ICreate; import ca.uhn.fhir.rest.gclient.IDelete; import ca.uhn.fhir.rest.gclient.IGetPage; @@ -100,6 +102,16 @@ public interface IGenericClient extends IRestfulClient { @Deprecated MethodOutcome delete(Class theType, String theId); + /** + * Force the client to fetch the server's conformance statement and validate that it is appropriate for this client. + * + * @throws FhirClientConnectionException + * if the conformance statement cannot be read, or if the client + * @throws FhirClientInnapropriateForServerException + * If the conformance statement indicates that the server is inappropriate for this client (e.g. it implements the wrong version of FHIR) + */ + void forceConformanceCheck() throws FhirClientConnectionException; + /** * Fluent method for the "get tags" operation */ @@ -114,17 +126,15 @@ public interface IGenericClient extends IRestfulClient { * Implementation of the "history instance" method. * * @param theType - * The type of resource to return the history for, or - * null to search for history across all resources + * The type of resource to return the history for, or null to search for history across all resources * @param theId - * The ID of the resource to return the history for, or null to search for all resource - * instances. Note that if this param is not null, theType must also not be null + * The ID of the resource to return the history for, or null to search for all resource instances. Note that if this param is not null, theType must also not + * be null * @param theSince * If not null, request that the server only return resources updated since this time * @param theLimit - * If not null, request that the server return no more than this number of resources. Note that the - * server may return less even if more are available, but should not return more according to the FHIR - * specification. + * If not null, request that the server return no more than this number of resources. Note that the server may return less even if more are available, but should not return more + * according to the FHIR specification. * @return A bundle containing returned resources * @deprecated As of 0.9, use the fluent {@link #history()} method instead */ @@ -135,49 +145,46 @@ public interface IGenericClient extends IRestfulClient { * Implementation of the "history instance" method. * * @param theType - * The type of resource to return the history for, or - * null to search for history across all resources + * The type of resource to return the history for, or null to search for history across all resources * @param theId - * The ID of the resource to return the history for, or null to search for all resource - * instances. Note that if this param is not null, theType must also not be null + * The ID of the resource to return the history for, or null to search for all resource instances. Note that if this param is not null, theType must also not + * be null * @param theSince * If not null, request that the server only return resources updated since this time * @param theLimit - * If not null, request that the server return no more than this number of resources. Note that the - * server may return less even if more are available, but should not return more according to the FHIR - * specification. + * If not null, request that the server return no more than this number of resources. Note that the server may return less even if more are available, but should not return more + * according to the FHIR specification. * @return A bundle containing returned resources * @deprecated As of 0.9, use the fluent {@link #history()} method instead */ @Deprecated Bundle history(Class theType, String theId, DateTimeDt theSince, Integer theLimit); + // /** + // * Implementation of the "instance read" method. This method will only ever do a "read" for the latest version of a + // * given resource instance, even if the ID passed in contains a version. If you wish to request a specific version + // * of a resource (the "vread" operation), use {@link #vread(Class, IdDt)} instead. + // *

+ // * Note that if an absolute resource ID is passed in (i.e. a URL containing a protocol and host as well as the + // * resource type and ID) the server base for the client will be ignored, and the URL passed in will be queried. + // *

+ // * + // * @param theType + // * The type of resource to load + // * @param theId + // * The ID to load, including the resource ID and the resource version ID. Valid values include + // * "Patient/123/_history/222", or "http://example.com/fhir/Patient/123/_history/222" + // * @return The resource + // */ + // T read(Class theType, IdDt theId); + /** - * Loads the previous/next bundle of resources from a paged set, using the link specified in the "link type=next" - * tag within the atom bundle. + * Loads the previous/next bundle of resources from a paged set, using the link specified in the "link type=next" tag within the atom bundle. * * @see Bundle#getLinkNext() */ IGetPage loadPage(); -// /** -// * Implementation of the "instance read" method. This method will only ever do a "read" for the latest version of a -// * given resource instance, even if the ID passed in contains a version. If you wish to request a specific version -// * of a resource (the "vread" operation), use {@link #vread(Class, IdDt)} instead. -// *

-// * Note that if an absolute resource ID is passed in (i.e. a URL containing a protocol and host as well as the -// * resource type and ID) the server base for the client will be ignored, and the URL passed in will be queried. -// *

-// * -// * @param theType -// * The type of resource to load -// * @param theId -// * The ID to load, including the resource ID and the resource version ID. Valid values include -// * "Patient/123/_history/222", or "http://example.com/fhir/Patient/123/_history/222" -// * @return The resource -// */ -// T read(Class theType, IdDt theId); - /** * Implementation of the FHIR "extended operations" action */ @@ -220,8 +227,7 @@ public interface IGenericClient extends IRestfulClient { IResource read(UriDt theUrl); /** - * 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. + * 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); @@ -250,8 +256,8 @@ public interface IGenericClient extends IRestfulClient { Bundle search(UriDt theUrl); /** - * If set to true, the client will log all requests and all responses. This is probably not a good - * production setting since it will result in a lot of extra logging, but it can be useful for troubleshooting. + * If set to true, the client will log all requests and all responses. This is probably not a good production setting since it will result in a lot of extra logging, but it can be + * useful for troubleshooting. * * @param theLogRequestAndResponse * Should requests and responses be logged @@ -268,8 +274,7 @@ public interface IGenericClient extends IRestfulClient { * * @param theResources * The resources to create/update in a single transaction - * @return A list of resource stubs (these will not be fully populated) containing IDs and other - * {@link IResource#getResourceMetadata() metadata} + * @return A list of resource stubs (these will not be fully populated) containing IDs and other {@link IResource#getResourceMetadata() metadata} * @deprecated Use {@link #transaction()} * */ @@ -277,8 +282,7 @@ public interface IGenericClient extends IRestfulClient { List transaction(List theResources); /** - * Remove an intercaptor that was previously registered using - * {@link IRestfulClient#registerInterceptor(IClientInterceptor)} + * Remove an intercaptor that was previously registered using {@link IRestfulClient#registerInterceptor(IClientInterceptor)} */ void unregisterInterceptor(IClientInterceptor theInterceptor); @@ -319,18 +323,16 @@ public interface IGenericClient extends IRestfulClient { MethodOutcome validate(IResource theResource); /** - * Implementation of the "instance vread" method. Note that this method expects theId to contain a - * resource ID as well as a version ID, and will fail if it does not. + * Implementation of the "instance vread" method. Note that this method expects theId to contain a resource ID as well as a version ID, and will fail if it does not. *

- * Note that if an absolute resource ID is passed in (i.e. a URL containing a protocol and host as well as the - * resource type and ID) the server base for the client will be ignored, and the URL passed in will be queried. + * Note that if an absolute resource ID is passed in (i.e. a URL containing a protocol and host as well as the resource type and ID) the server base for the client will be ignored, and the URL + * passed in will be queried. *

* * @param theType * The type of resource to load * @param theId - * The ID to load, including the resource ID and the resource version ID. Valid values include - * "Patient/123/_history/222", or "http://example.com/fhir/Patient/123/_history/222" + * The ID to load, including the resource ID and the resource version ID. Valid values include "Patient/123/_history/222", or "http://example.com/fhir/Patient/123/_history/222" * @return The resource */ T vread(Class theType, IdDt theId); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/RestfulClientFactory.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/RestfulClientFactory.java index 0a45bfafb292..b033d8864f0a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/RestfulClientFactory.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/RestfulClientFactory.java @@ -50,6 +50,7 @@ import ca.uhn.fhir.model.base.resource.BaseConformance; import ca.uhn.fhir.rest.client.api.IRestfulClient; import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; +import ca.uhn.fhir.rest.client.exceptions.FhirClientInnapropriateForServerException; import ca.uhn.fhir.rest.method.BaseMethodBinding; import ca.uhn.fhir.rest.server.Constants; @@ -193,25 +194,29 @@ public synchronized IGenericClient newGenericClient(String theServerBase) { /** * This method is internal to HAPI - It may change in future versions, use with caution. */ - public void validateServerBaseIfConfiguredToDoSo(String theServerBase, HttpClient theHttpClient) { - String serverBase = theServerBase; - if (!serverBase.endsWith("/")) { - serverBase = serverBase + "/"; - } + public void validateServerBaseIfConfiguredToDoSo(String theServerBase, HttpClient theHttpClient, BaseClient theClient) { + String serverBase = normalizeBaseUrlForMap(theServerBase); switch (myServerValidationMode) { case NEVER: break; case ONCE: if (!myValidatedServerBaseUrls.contains(serverBase)) { - validateServerBase(serverBase, theHttpClient); - myValidatedServerBaseUrls.add(serverBase); + validateServerBase(serverBase, theHttpClient, theClient); } break; } } + private String normalizeBaseUrlForMap(String theServerBase) { + String serverBase = theServerBase; + if (!serverBase.endsWith("/")) { + serverBase = serverBase + "/"; + } + return serverBase; + } + @Override public synchronized void setConnectionRequestTimeout(int theConnectionRequestTimeout) { myConnectionRequestTimeout = theConnectionRequestTimeout; @@ -267,9 +272,12 @@ public synchronized void setSocketTimeout(int theSocketTimeout) { myHttpClient = null; } - private void validateServerBase(String theServerBase, HttpClient theHttpClient) { + void validateServerBase(String theServerBase, HttpClient theHttpClient, BaseClient theClient) { GenericClient client = new GenericClient(myContext, theHttpClient, theServerBase, this); + for (IClientInterceptor interceptor : theClient.getInterceptors()) { + client.registerInterceptor(interceptor); + } client.setDontValidateConformance(true); BaseConformance conformance; @@ -299,9 +307,12 @@ private void validateServerBase(String theServerBase, HttpClient theHttpClient) if (serverFhirVersionEnum != null) { FhirVersionEnum contextFhirVersion = myContext.getVersion().getVersion(); if (!contextFhirVersion.isEquivalentTo(serverFhirVersionEnum)) { - throw new FhirClientConnectionException(myContext.getLocalizer().getMessage(RestfulClientFactory.class, "wrongVersionInConformance", theServerBase + Constants.URL_TOKEN_METADATA, serverFhirVersionString, serverFhirVersionEnum, contextFhirVersion)); + throw new FhirClientInnapropriateForServerException(myContext.getLocalizer().getMessage(RestfulClientFactory.class, "wrongVersionInConformance", theServerBase + Constants.URL_TOKEN_METADATA, serverFhirVersionString, serverFhirVersionEnum, contextFhirVersion)); } } + + myValidatedServerBaseUrls.add(normalizeBaseUrlForMap(theServerBase)); + } @Override diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/exceptions/FhirClientInnapropriateForServerException.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/exceptions/FhirClientInnapropriateForServerException.java new file mode 100644 index 000000000000..1e59d6dc0d98 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/exceptions/FhirClientInnapropriateForServerException.java @@ -0,0 +1,46 @@ +package ca.uhn.fhir.rest.client.exceptions; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2015 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.rest.server.exceptions.BaseServerResponseException; + +/** + * This exception will be thrown by FHIR clients if the client attempts to + * communicate with a server which is a valid FHIR server but is incompatible + * with this client for some reason. + */ +public class FhirClientInnapropriateForServerException extends BaseServerResponseException { + + private static final long serialVersionUID = 1L; + + public FhirClientInnapropriateForServerException(Throwable theCause) { + super(0, theCause); + } + + public FhirClientInnapropriateForServerException(String theMessage, Throwable theCause) { + super(0, theMessage, theCause); + } + + public FhirClientInnapropriateForServerException(String theMessage) { + super(0, theMessage); + } + +} diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/ClientServerValidationTestDstu2.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/ClientServerValidationTestDstu2.java index 2a3a7ebcb2ce..c59553071ad1 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/ClientServerValidationTestDstu2.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/ClientServerValidationTestDstu2.java @@ -198,4 +198,52 @@ public InputStream answer(InvocationOnMock theInvocation) throws Throwable { assertEquals("Basic VVNFUjpQQVNT", auth.getValue()); } + @Test + public void testForceConformanceCheck() throws Exception { + Conformance conf = new Conformance(); + conf.setFhirVersion("0.5.0"); + final String confResource = myCtx.newXmlParser().encodeResourceToString(conf); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + if (myFirstResponse) { + myFirstResponse = false; + return new ReaderInputStream(new StringReader(confResource), Charset.forName("UTF-8")); + } else { + Patient resource = new Patient(); + resource.addName().addFamily().setValue("FAM"); + return new ReaderInputStream(new StringReader(myCtx.newXmlParser().encodeResourceToString(resource)), Charset.forName("UTF-8")); + } + } + }); + + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + + myCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.ONCE); + + IGenericClient client = myCtx.newRestfulGenericClient("http://foo"); + client.registerInterceptor(new BasicAuthInterceptor("USER", "PASS")); + + client.forceConformanceCheck(); + + assertEquals(1, capt.getAllValues().size()); + + Patient pt = (Patient) client.read(new UriDt("http://foo/Patient/123")); + assertEquals("FAM", pt.getNameFirstRep().getFamilyAsSingleString()); + + assertEquals(2, capt.getAllValues().size()); + + Header auth = capt.getAllValues().get(0).getFirstHeader("Authorization"); + assertNotNull(auth); + assertEquals("Basic VVNFUjpQQVNT", auth.getValue()); + auth = capt.getAllValues().get(1).getFirstHeader("Authorization"); + assertNotNull(auth); + assertEquals("Basic VVNFUjpQQVNT", auth.getValue()); + } + } diff --git a/hapi-fhir-testpage-interceptor/pom.xml b/hapi-fhir-testpage-interceptor/pom.xml index f0e9df7d4daf..5c66362e4579 100644 --- a/hapi-fhir-testpage-interceptor/pom.xml +++ b/hapi-fhir-testpage-interceptor/pom.xml @@ -4,7 +4,7 @@ ca.uhn.hapi.fhir hapi-fhir - 0.9-SNAPSHOT + 1.0-SNAPSHOT ../pom.xml @@ -27,7 +27,7 @@ ca.uhn.hapi.fhir hapi-fhir-base - 0.9-SNAPSHOT + 1.0-SNAPSHOT org.thymeleaf @@ -38,7 +38,7 @@ javax.servlet javax.servlet-api - 3.1.0 + ${servlet_api_version} provided diff --git a/src/changes/changes.xml b/src/changes/changes.xml index c33acf94e847..5daa8d8f3080 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -168,6 +168,15 @@ Make BaseElement#getUndeclaredExtensions() and BaseElement#getUndeclaredExtensions() return a mutable list so that it is possible to delete extensions from a resource instance. + + Server conformance statement check in clients (this is the check + where the first time a given FhirContext is used to access a given server + base URL, it will first check the server's Conformance statement to ensure + that it supports the correct version of FHIR) now uses any + registered client interceptors. In addition, IGenericClient now has a method + "forceConformanceCheck()" which manually triggers this check. Thanks to + Doug Martin for reporting and suggesting! +