Correctly handle contained resources in client

This commit is contained in:
James Agnew 2014-08-05 17:06:35 -04:00
parent aef7ea5e9f
commit 2c44a4f76e
11 changed files with 476 additions and 34 deletions

View File

@ -53,6 +53,13 @@
<version>${mitreid-connect-version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.0.2.RELEASE</version>
<optional>true</optional>
</dependency>
<!-- Only required for narrative generator support -->
<dependency>
@ -277,7 +284,6 @@
<escapeHTML>false</escapeHTML>
</configuration>
</plugin>
<!--
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
@ -328,7 +334,6 @@
</reportSet>
</reportSets>
</plugin>
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>

View File

@ -1126,6 +1126,31 @@ class ParserState<T> {
@Override
public void wereBack() {
myObject = (T) myInstance;
/*
* Stitch together resource references
*/
Map<String, IResource> idToResource = new HashMap<String, IResource>();
List<IResource> resources = myInstance.toListOfResources();
for (IResource next : resources) {
if (next.getId() != null && next.getId().isEmpty()==false) {
idToResource.put(next.getId().toUnqualifiedVersionless().getValue(), next);
}
}
for (IResource next : resources) {
List<ResourceReferenceDt> refs = myContext.newTerser().getAllPopulatedChildElementsOfType(next, ResourceReferenceDt.class);
for (ResourceReferenceDt nextRef : refs) {
if (nextRef.isEmpty()==false && nextRef.getReference() != null) {
IResource target = idToResource.get(nextRef.getReference().getValue());
if (target != null) {
nextRef.setResource(target);
}
}
}
}
}
}
@ -1361,25 +1386,6 @@ class ParserState<T> {
case INITIAL:
throw new DataFormatException("Unexpected attribute: " + theValue);
case REFERENCE:
// int lastSlash = theValue.lastIndexOf('/');
// if (lastSlash==-1) {
// myInstance.setResourceId(theValue);
// } else if (lastSlash==0) {
// myInstance.setResourceId(theValue.substring(1));
// }else {
// int secondLastSlash=theValue.lastIndexOf('/', lastSlash-1);
// String resourceTypeName;
// if (secondLastSlash==-1) {
// resourceTypeName=theValue.substring(0,lastSlash);
// }else {
// resourceTypeName=theValue.substring(secondLastSlash+1,lastSlash);
// }
// myInstance.setResourceId(theValue.substring(lastSlash+1));
// RuntimeResourceDefinition def = myContext.getResourceDefinition(resourceTypeName);
// if(def!=null) {
// myInstance.setResourceType(def.getImplementingClass());
// }
// }
myInstance.setReference(theValue);
break;
}

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.rest.server.security;
/*
* #%L
* HAPI FHIR - Core 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%
*/
public interface ISecurityOutcome {
}

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.rest.server.security;
/*
* #%L
* HAPI FHIR - Core 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.text.ParseException;
import java.util.Date;

View File

@ -58,14 +58,16 @@
<menu name="Documentation">
<item name="Introduction" href="./doc_intro.html" />
<item name="The Data Model" href="./doc_fhirobjects.html" />
<item name="RESTful Server" href="./doc_rest_server.html" >
<item name="FHIR Tester App" href="./doc_server_tester.html" />
<item name="The Data Model" href="./doc_fhirobjects.html">
<item name="Extensions" href="./doc_extensions.html" />
<item name="Resource References" href="./doc_resource_references.html" />
</item>
<item name="RESTful Client" href="./doc_rest_client.html" />
<item name="RESTful Operations" href="./doc_rest_operations.html" />
<item name="Extensions" href="./doc_extensions.html" />
<item name="Narrative Generator" href="./doc_narrative.html" />
<item name="RESTful Server" href="./doc_rest_server.html" >
<item name="RESTful Operations" href="./doc_rest_operations.html" />
<item name="FHIR Tester App" href="./doc_server_tester.html" />
<item name="Narrative Generator" href="./doc_narrative.html" />
</item>
<item name="Logging" href="./doc_logging.html" />
<item name="Tinder Plugin" href="./doc_tinder.html" />
</menu>

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
<properties>
<title>Resource References - HAPI FHIR</title>
<author email="jamesagnew@users.sourceforge.net">James Agnew</author>
</properties>
<body>
<!-- The body of the document contains a number of sections -->
<section name="Resource References">
<macro name="toc">
</macro>
<p>
Resource references are a key part of the HAPI FHIR model,
since almost any resource will have references to other resources
within it.
</p>
<p>
The <a href="http://jamesagnew.github.io/hapi-fhir/apidocs/ca/uhn/fhir/model/dstu/composite/ResourceReferenceDt.html">ResourceReferenceDt</a>
type is the datatype for references. This datatype has a number of properties which help
make working with FHIR simple.
</p>
<p>
The <code>getReference()</code> method returns an IdDt instance which contains the identity of the
resource being referenced. This is the item which is most commonly populated when
interacting with FHIR. For example, consider the following Patient resource, which
contains a reference to an Organization resource:
</p>
<source><![CDATA[<Patient xmlns="http://hl7.org/fhir">
<identifier>
<system value="urn:mrns"/>
<value value="253345"/>
</identifier>
<managingOrganization>
<reference value="Organization/112"/>
</managingOrganization>
</Patient>]]></source>
<p>
Given a Patient resource obtained by invoking a client operation, a call to
<code>IdDt ref = patient.getManagingOrganization().getReference();</code>
returns an instance of IdDt which contains the "Organization/112" reference.
</p>
<p>
ResourceReferenceDt also has a field for storing actual resource instances however,
and this can be very useful.
</p>
</section>
<section name="References in Client Code">
<p>
In client code, if a resource reference refers to a resource which was received as a
part of the same response, <code>getResource()</code> will be populated with the
actual resource. This can happen because either the resource was received as a
contained resource, or the resource was received as a separate resource in a bundle.
</p>
</section>
<section name="References in Server Code">
<p>
In server code, you may wish to return "contained" resources. A simple way to do this
is to add these resources directly to the reference field and ensure that the resource
has no ID populated. When this condition is detected, HAPI will automatically create a local
reference ID and add the resource to the "contained" section when it encodes the resource.
</p>
<p>
The following example shows a Patient resource being created which will have a
contained resource as its managing organization when encoded from a server:
</p>
<source><![CDATA[Organization containedOrg = new Organization();
containedOrg.getName().setValue("Contained Test Organization");
Patient patient = new Patient();
patient.setId("Patient/1333");
patient.addIdentifier("urn:mrns", "253345");
patient.getManagingOrganization().setResource(patient);]]></source>
<subsection name="Handling Includes (_include) in a Bundle">
<p>
Your server code may also wish to add additional resource to a bundle
being returned (e.g. because of an _include directive in the client's request).
</p>
<p>
To do this, you can implement your server method to simply return
<code>List&lt;IResource&gt;</code> and then simply add your extra resources to
the list. Another technique however, is to populate the reference as shown
in the example above, but ensure that the referenced resource has an ID set.
</p>
<p>
In the following example, the Organization resource has an ID set, so it will not
be contained but will rather appear as a distinct entry in any returned
bundles.
</p>
<source><![CDATA[Organization org = new Organization();
org.setId("Organization/65546");
org.getName().setValue("Contained Test Organization");
Patient patient = new Patient();
patient.setId("Patient/1333");
patient.addIdentifier("urn:mrns", "253345");
patient.getManagingOrganization().setResource(patient);]]></source>
</subsection>
</section>
</body>
</document>

View File

@ -177,6 +177,31 @@
on your server.
</p>
<subsection name="Enhancing the Generated Conformance Statement">
<p>
If you have a need to add your own content (special extensions, etc.) to your
server's conformance statement, but still want to take advantage of HAPI's automatic
conformance generation, you may wish to extend
<a href="http://jamesagnew.github.io/hapi-fhir/apidocs/ca/uhn/fhir/rest/server/provider/ServerConformanceProvider.html">ServerConformanceProvider</a>.
</p>
<p>
In your own class extending this class, you can override the <code>getServerConformance()</code> method
to provide your own implementation. In this method, call
<code>super.getServerConformance()</code> to obtain the built-in conformance statement and then
add your own information to it.
</p>
<p>
Note that if you are adding items during each invocation you should be aware that by default the
same instance is cached by ServerConformanceProvider. This can result in an ever-growing
conformance statement. You must call <code>setCache(false);</code> in
the constructor of your new conformance provider to avoid this behaviour.
</p>
</subsection>
</section>
<section name="Paging Responses">

View File

@ -413,6 +413,7 @@ public class XmlParserTest {
@Test
public void testEncodeContainedResources() {

View File

@ -0,0 +1,141 @@
package ca.uhn.fhir.rest.client;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.List;
import org.apache.commons.io.input.ReaderInputStream;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicStatusLine;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.ExtensionDt;
import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu.resource.Conformance;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.rest.server.Constants;
public class IncludedResourceStitchingClientTest {
private FhirContext ctx;
private HttpClient httpClient;
private HttpResponse httpResponse;
// atom-document-large.xml
@Before
public void before() {
ctx = new FhirContext(Patient.class, Conformance.class);
httpClient = mock(HttpClient.class, new ReturnsDeepStubs());
ctx.getRestfulClientFactory().setHttpClient(httpClient);
httpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
}
@Test
public void testWithParam() 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), 200, "OK"));
when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_ATOM_XML + "; charset=UTF-8"));
when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(createBundle()), Charset.forName("UTF-8")));
IGenericClient client = ctx.newRestfulGenericClient( "http://foo");
Bundle bundle = client.search().forResource("Patient").execute();
assertEquals(HttpGet.class, capt.getValue().getClass());
HttpGet get = (HttpGet) capt.getValue();
assertEquals("http://foo/Patient", get.getURI().toString());
assertEquals(3, bundle.size());
Patient p = (Patient) bundle.getEntries().get(0).getResource();
List<ExtensionDt> exts = p.getUndeclaredExtensionsByUrl("http://foo");
assertEquals(1,exts.size());
ExtensionDt ext = exts.get(0);
ResourceReferenceDt ref = (ResourceReferenceDt) ext.getValue();
assertEquals("Organization/o1", ref.getReference().getValue());
assertNotNull(ref.getResource());
}
private String createBundle() {
//@formatter:on
return "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" +
" <title/>\n" +
" <id>f051fd86-4daa-48da-80f7-5a0443bf6f11</id>\n" +
" <link rel=\"self\" href=\"http://localhost:49627/Patient?_query=extInclude&amp;_pretty=true\"/>\n" +
" <link rel=\"fhir-base\" href=\"http://localhost:49627\"/>\n" +
" <os:totalResults xmlns:os=\"http://a9.com/-/spec/opensearch/1.1/\">2</os:totalResults>\n" +
" <published>2014-08-05T15:22:08.512-04:00</published>\n" +
" <author>\n" +
" <name>HAPI FHIR Server</name>\n" +
" </author>\n" +
" <entry>\n" +
" <title>Patient p1</title>\n" +
" <id>http://localhost:49627/Patient/p1</id>\n" +
" <published>2014-08-05T15:22:08-04:00</published>\n" +
" <link rel=\"self\" href=\"http://localhost:49627/Patient/p1\"/>\n" +
" <content type=\"text/xml\">\n" +
" <Patient xmlns=\"http://hl7.org/fhir\">\n" +
" <extension url=\"http://foo\">\n" +
" <valueResource>\n" +
" <reference value=\"Organization/o1\"/>\n" +
" </valueResource>\n" +
" </extension>\n" +
" <identifier>\n" +
" <label value=\"p1\"/>\n" +
" </identifier>\n" +
" </Patient>\n" +
" </content>\n" +
" </entry>\n" +
" <entry>\n" +
" <title>Patient p2</title>\n" +
" <id>http://localhost:49627/Patient/p2</id>\n" +
" <published>2014-08-05T15:22:08-04:00</published>\n" +
" <link rel=\"self\" href=\"http://localhost:49627/Patient/p2\"/>\n" +
" <content type=\"text/xml\">\n" +
" <Patient xmlns=\"http://hl7.org/fhir\">\n" +
" <extension url=\"http://foo\">\n" +
" <valueResource>\n" +
" <reference value=\"Organization/o1\"/>\n" +
" </valueResource>\n" +
" </extension>\n" +
" <identifier>\n" +
" <label value=\"p2\"/>\n" +
" </identifier>\n" +
" </Patient>\n" +
" </content>\n" +
" </entry>\n" +
" <entry>\n" +
" <title>Organization o1</title>\n" +
" <id>http://localhost:49627/Organization/o1</id>\n" +
" <published>2014-08-05T15:22:08-04:00</published>\n" +
" <link rel=\"self\" href=\"http://localhost:49627/Organization/o1\"/>\n" +
" <content type=\"text/xml\">\n" +
" <Organization xmlns=\"http://hl7.org/fhir\">\n" +
" <name value=\"o1\"/>\n" +
" </Organization>\n" +
" </content>\n" +
" </entry>\n" +
"</feed>";
//@formatter:off
}
}

View File

@ -25,6 +25,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Bundle;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu.resource.Organization;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
@ -43,6 +44,7 @@ public class IncludeTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(IncludeTest.class);
private static int ourPort;
private static Server ourServer;
private static FhirContext ourCtx;
@Test
public void testNoIncludes() throws Exception {
@ -52,7 +54,7 @@ public class IncludeTest {
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = new FhirContext().newXmlParser().parseBundle(responseContent);
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(1, bundle.size());
Patient p = bundle.getResources(Patient.class).get(0);
@ -68,7 +70,7 @@ public class IncludeTest {
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = new FhirContext().newXmlParser().parseBundle(responseContent);
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(1, bundle.size());
Patient p = bundle.getResources(Patient.class).get(0);
@ -85,7 +87,7 @@ public class IncludeTest {
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = new FhirContext().newXmlParser().parseBundle(responseContent);
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
ourLog.info(responseContent);
@ -102,6 +104,60 @@ public class IncludeTest {
}
@Test
public void testIIncludedResourcesNonContainedInExtension() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=extInclude&_pretty=true");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
ourLog.info(responseContent);
assertEquals(3, bundle.size());
assertEquals(new IdDt("Patient/p1"), bundle.toListOfResources().get(0).getId().toUnqualifiedVersionless());
assertEquals(new IdDt("Patient/p2"), bundle.toListOfResources().get(1).getId().toUnqualifiedVersionless());
assertEquals(new IdDt("Organization/o1"), bundle.toListOfResources().get(2).getId().toUnqualifiedVersionless());
Patient p1 = (Patient) bundle.toListOfResources().get(0);
assertEquals(0,p1.getContained().getContainedResources().size());
Patient p2 = (Patient) bundle.toListOfResources().get(1);
assertEquals(0,p2.getContained().getContainedResources().size());
}
@Test
public void testIIncludedResourcesNonContainedInExtensionJson() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=extInclude&_pretty=true&_format=json");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = ourCtx.newJsonParser().parseBundle(responseContent);
ourLog.info(responseContent);
assertEquals(3, bundle.size());
assertEquals(new IdDt("Patient/p1"), bundle.toListOfResources().get(0).getId().toUnqualifiedVersionless());
assertEquals(new IdDt("Patient/p2"), bundle.toListOfResources().get(1).getId().toUnqualifiedVersionless());
assertEquals(new IdDt("Organization/o1"), bundle.toListOfResources().get(2).getId().toUnqualifiedVersionless());
Patient p1 = (Patient) bundle.toListOfResources().get(0);
assertEquals(0,p1.getContained().getContainedResources().size());
Patient p2 = (Patient) bundle.toListOfResources().get(1);
assertEquals(0,p2.getContained().getContainedResources().size());
}
@Test
public void testTwoInclude() throws Exception {
@ -111,7 +167,7 @@ public class IncludeTest {
IOUtils.closeQuietly(status.getEntity().getContent());
assertEquals(200, status.getStatusLine().getStatusCode());
Bundle bundle = new FhirContext().newXmlParser().parseBundle(responseContent);
Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent);
assertEquals(1, bundle.size());
Patient p = bundle.getResources(Patient.class).get(0);
@ -135,6 +191,8 @@ public class IncludeTest {
@BeforeClass
public static void beforeClass() throws Exception {
ourCtx = new FhirContext();
ourPort = RandomServerPortProvider.findFreePort();
ourServer = new Server(ourPort);
@ -142,6 +200,7 @@ public class IncludeTest {
ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer();
servlet.setFhirContext(ourCtx);
servlet.setResourceProviders(patientProvider);
ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
@ -179,6 +238,25 @@ public class IncludeTest {
return Arrays.asList(p1, p2);
}
@Search(queryName = "extInclude")
public List<Patient> extInclude() {
Organization o1 = new Organization();
o1.getName().setValue("o1");
o1.setId("o1");
Patient p1 = new Patient();
p1.setId("p1");
p1.addIdentifier().setLabel("p1");
p1.addUndeclaredExtension(false, "http://foo", new ResourceReferenceDt(o1));
Patient p2 = new Patient();
p2.setId("p2");
p2.addIdentifier().setLabel("p2");
p2.addUndeclaredExtension(false, "http://foo", new ResourceReferenceDt(o1));
return Arrays.asList(p1, p2);
}
@Search(queryName = "containedInclude")
public List<Patient> containedInclude() {
Organization o1 = new Organization();
@ -223,4 +301,23 @@ public class IncludeTest {
}
public static void main(String[] args) {
Organization org = new Organization();
org.setId("Organization/65546");
org.getName().setValue("Contained Test Organization");
Patient patient = new Patient();
patient.setId("Patient/1333");
patient.addIdentifier("urn:mrns", "253345");
patient.getManagingOrganization().setResource(patient);
System.out.println(new FhirContext().newXmlParser().setPrettyPrint(true).encodeResourceToString(patient));
patient.getManagingOrganization().getReference();
}
}

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.rest.server.security;
import static org.junit.Assert.*;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -11,11 +12,9 @@ import javax.servlet.http.HttpServletRequest;
import org.hamcrest.core.StringContains;
import org.junit.Test;
import org.mitre.jose.keystore.JWKSetKeyStore;
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
import org.mitre.jwt.signer.service.impl.DefaultJwtSigningAndValidationService;
import org.mitre.jwt.signer.service.impl.JWKSetCacheService;
import org.mitre.oauth2.model.RegisteredClient;
import org.mitre.openid.connect.client.service.impl.DynamicRegistrationClientConfigurationService;
import org.mitre.openid.connect.client.service.impl.StaticClientConfigurationService;
import org.mitre.openid.connect.client.service.impl.StaticServerConfigurationService;
import org.mitre.openid.connect.config.ServerConfiguration;