Fix #168 - Client conformance check should use any registered client

interceptors
This commit is contained in:
James Agnew 2015-04-29 20:02:01 -04:00
parent b68ce95b3f
commit cb7d94841e
8 changed files with 191 additions and 62 deletions

View File

@ -153,10 +153,14 @@ public abstract class BaseClient implements IRestfulClient {
return invokeClient(theContext, binding, clientInvocation, null, null, theLogRequestAndResponse);
}
void forceConformanceCheck() {
myFactory.validateServerBase(myUrlBase, myClient, this);
}
<T> T invokeClient(FhirContext theContext, IClientResponseHandler<T> 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 abstract class BaseClient implements IRestfulClient {
return reader;
}
public List<IClientInterceptor> getInterceptors() {
return Collections.unmodifiableList(myInterceptors);
}
}

View File

@ -140,7 +140,7 @@ public class GenericClient extends BaseClient implements IGenericClient {
super(theHttpClient, theServerBase, theFactory);
myContext = theContext;
}
@Override
public BaseConformance conformance() {
HttpGetClientInvocation invocation = MethodUtil.createConformanceInvocation();
@ -156,6 +156,11 @@ public class GenericClient extends BaseClient implements IGenericClient {
return resp;
}
@Override
public void forceConformanceCheck() {
super.forceConformanceCheck();
}
@Override
public ICreate create() {
return new CreateInternal();

View File

@ -34,6 +34,8 @@ import ca.uhn.fhir.model.primitive.IdDt;
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<? extends IResource> 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
* <code>null</code> to search for history across all resources
* The type of resource to return the history for, or <code>null</code> to search for history across all resources
* @param theId
* The ID of the resource to return the history for, or <code>null</code> to search for all resource
* instances. Note that if this param is not null, <code>theType</code> must also not be null
* The ID of the resource to return the history for, or <code>null</code> to search for all resource instances. Note that if this param is not null, <code>theType</code> 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
* <code>null</code> to search for history across all resources
* The type of resource to return the history for, or <code>null</code> to search for history across all resources
* @param theId
* The ID of the resource to return the history for, or <code>null</code> to search for all resource
* instances. Note that if this param is not null, <code>theType</code> must also not be null
* The ID of the resource to return the history for, or <code>null</code> to search for all resource instances. Note that if this param is not null, <code>theType</code> 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
<T extends IResource> Bundle history(Class<T> 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.
// * <p>
// * 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.
// * </p>
// *
// * @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 extends IBaseResource> T read(Class<T> 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.
// * <p>
// * 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.
// * </p>
// *
// * @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 extends IBaseResource> T read(Class<T> 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 <code>true</code>, 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 <code>true</code>, 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 (<b>these will not be fully populated</b>) containing IDs and other
* {@link IResource#getResourceMetadata() metadata}
* @return A list of resource stubs (<b>these will not be fully populated</b>) containing IDs and other {@link IResource#getResourceMetadata() metadata}
* @deprecated Use {@link #transaction()}
*
*/
@ -277,8 +282,7 @@ public interface IGenericClient extends IRestfulClient {
List<IResource> transaction(List<IResource> 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 <code>theId</code> 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 <code>theId</code> to contain a resource ID as well as a version ID, and will fail if it does not.
* <p>
* 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.
* </p>
*
* @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 extends IBaseResource> T vread(Class<T> theType, IdDt theId);

View File

@ -50,6 +50,7 @@ import ca.uhn.fhir.context.FhirVersionEnum;
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 class RestfulClientFactory implements IRestfulClientFactory {
/**
* 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 class RestfulClientFactory implements IRestfulClientFactory {
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 @@ public class RestfulClientFactory implements IRestfulClientFactory {
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

View File

@ -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);
}
}

View File

@ -198,4 +198,52 @@ public class ClientServerValidationTestDstu2 {
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<HttpUriRequest> 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<InputStream>() {
@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());
}
}

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>0.9-SNAPSHOT</version>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -27,7 +27,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-base</artifactId>
<version>0.9-SNAPSHOT</version>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.thymeleaf</groupId>
@ -38,7 +38,7 @@
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<version>${servlet_api_version}</version>
<scope>provided</scope>
</dependency>

View File

@ -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.
</action>
<action type="fix" issue="168">
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!
</action>
</release>
<release version="0.9" date="2015-Mar-14">
<action type="add">