diff --git a/examples/src/main/java/example/RestfulPatientResourceProviderMore.java b/examples/src/main/java/example/RestfulPatientResourceProviderMore.java index 5b777c80e73..6121b06febf 100644 --- a/examples/src/main/java/example/RestfulPatientResourceProviderMore.java +++ b/examples/src/main/java/example/RestfulPatientResourceProviderMore.java @@ -10,6 +10,8 @@ import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.hl7.fhir.dstu3.model.IdType; + import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.BundleEntry; @@ -108,6 +110,23 @@ public List getAllOrganizations() { } //END SNIPPET: searchAll +//START SNIPPET: updateEtag +@Update +public MethodOutcome update(@IdParam IdType theId, @ResourceParam Patient thePatient) { + String resourceId = theId.getIdPart(); + String versionId = theId.getVersionIdPart(); // this will contain the ETag + + String currentVersion = "1"; // populate this with the current version + + if (!versionId.equals(currentVersion)) { + throw new ResourceVersionConflictException("Expected version " + currentVersion); + } + + // ... perform the update ... + return new MethodOutcome(); + +} +//END SNIPPET: updateEtag //START SNIPPET: summaryAndElements @Search diff --git a/hapi-fhir-android/pom.xml b/hapi-fhir-android/pom.xml index 9d39212b645..8074aa23b7e 100644 --- a/hapi-fhir-android/pom.xml +++ b/hapi-fhir-android/pom.xml @@ -35,10 +35,6 @@ commons-codec commons-codec - - org.codehaus.woodstox - woodstox-core-asl - @@ -59,6 +55,11 @@ 2.1-SNAPSHOT true + + org.codehaus.woodstox + woodstox-core-asl + true + org.apache.httpcomponents httpclient-android @@ -75,10 +76,6 @@ commons-io commons-io - - commons-codec - commons-codec - org.apache.commons commons-lang3 @@ -98,25 +95,59 @@ org.apache.maven.plugins maven-failsafe-plugin - + dstu2_shade integration-test verify + + + **/*Dstu2ShadeIT.java + + + ${basedir}/target/hapi-fhir-android-${project.version}-dstu2.jar + + + ca.uhn.hapi.fhir:* + org.codehaus.woodstox:woodstox-core-asl + org.codehaus.woodstox:stax2-api + + + + + dstu2 + + integration-test + verify + + + + **/*Dstu2IT.java + + + org.codehaus.woodstox:woodstox-core-asl + org.codehaus.woodstox:stax2-api + + + + + dstu3 + + integration-test + verify + + + + **/*Dstu3IT.java + + + org.codehaus.woodstox:woodstox-core-asl + org.codehaus.woodstox:stax2-api + + @@ -130,6 +161,9 @@ ca.uhn.hapi.fhir:hapi-fhir-base + org.codehaus.woodstox:woodstox-core-asl + javax.xml.stream:stax-api + org.codehaus.woodstox:stax2-api @@ -141,6 +175,10 @@ javax.json ca.uhn.fhir.repackage.javax.json + + com.ctc.wstx.stax + ca.uhn.fhir.repackage.com.ctc.wstx.stax + @@ -182,8 +220,8 @@ package shade - - + + dstu @@ -261,6 +299,27 @@ + + org.jacoco + jacoco-maven-plugin + + + ${basedir}/target/classes + + + ${basedir}/src/main/java + + true + + + + default-prepare-agent + + prepare-agent + + + + diff --git a/hapi-fhir-android/src/test/java/ca/uhn/fhir/android/BuiltJarDstu2IT.java b/hapi-fhir-android/src/test/java/ca/uhn/fhir/android/BuiltJarDstu2IT.java new file mode 100644 index 00000000000..2ac7845d43b --- /dev/null +++ b/hapi-fhir-android/src/test/java/ca/uhn/fhir/android/BuiltJarDstu2IT.java @@ -0,0 +1,192 @@ +package ca.uhn.fhir.android; + +import static org.junit.Assert.*; + +import java.io.File; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import javax.naming.ConfigurationException; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.WildcardFileFilter; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.dstu2.composite.QuantityDt; +import ca.uhn.fhir.model.dstu2.resource.Observation; +import ca.uhn.fhir.model.dstu2.resource.Patient; +import ca.uhn.fhir.rest.client.IGenericClient; +import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; + +public class BuiltJarDstu2IT { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BuiltJarDstu2IT.class); + + @BeforeClass + public static void beforeClass() { + System.setProperty("javax.xml.stream.XMLInputFactory", "FOO"); + System.setProperty("javax.xml.stream.XMLOutputFactory", "FOO"); + } + + @Test + public void testParserXml() throws Exception { + + FhirContext ctx = FhirContext.forDstu2(); + + Patient p = new Patient(); + p.addIdentifier().setSystem("system"); + + try { + ctx.newXmlParser().encodeResourceToString(p); + fail(); + } catch (ca.uhn.fhir.context.ConfigurationException e) { + assertEquals("Unable to initialize StAX - XML processing is disabled",e.getMessage()); + } + } + + @Test + public void testParserJson() { + + FhirContext ctx = FhirContext.forDstu2(); + + Observation o = new Observation(); + o.getCode().setText("TEXT"); + o.setValue(new QuantityDt(123)); + o.addIdentifier().setSystem("system"); + + String str = ctx.newJsonParser().encodeResourceToString(o); + Observation p2 = ctx.newJsonParser().parseResource(Observation.class, str); + + assertEquals("TEXT", p2.getCode().getText()); + + QuantityDt dt = (QuantityDt) p2.getValue(); + dt.getComparatorElement().getValueAsEnum(); + + } + + /** + * A simple client test - We try to connect to a server that doesn't exist, but + * if we at least get the right exception it means we made it up to the HTTP/network stack + * + * Disabled for now - TODO: add the old version of the apache client (the one that + * android uses) and see if this passes + */ + @SuppressWarnings("deprecation") + public void testClient() { + FhirContext ctx = FhirContext.forDstu2(); + try { + IGenericClient client = ctx.newRestfulGenericClient("http://127.0.0.1:44442/SomeBase"); + client.conformance(); + } catch (FhirClientConnectionException e) { + // this is good + } + } + + /** + * Android does not like duplicate entries in the JAR + */ + @Test + public void testJarContents() throws Exception { + String wildcard = "hapi-fhir-android-*.jar"; + Collection files = FileUtils.listFiles(new File("target"), new WildcardFileFilter(wildcard), null); + if (files.isEmpty()) { + throw new Exception("No files matching " + wildcard); + } + + for (File file : files) { + if (file.getName().endsWith("sources.jar")) { + continue; + } + if (file.getName().endsWith("javadoc.jar")) { + continue; + } + if (file.getName().contains("original.jar")) { + continue; + } + + ourLog.info("Testing file: {}", file); + + ZipFile zip = new ZipFile(file); + + int totalClasses = 0; + int totalMethods = 0; + TreeSet topMethods = new TreeSet(); + + try { + Set names = new HashSet(); + for (Enumeration iter = zip.entries(); iter.hasMoreElements();) { + ZipEntry next = iter.nextElement(); + String nextName = next.getName(); + if (!names.add(nextName)) { + throw new Exception("File " + file + " contains duplicate contents: " + nextName); + } + + if (nextName.contains("$") == false) { + if (nextName.endsWith(".class")) { + String className = nextName.replace("/", ".").replace(".class", ""); + try { + Class clazz = Class.forName(className); + int methodCount = clazz.getMethods().length; + topMethods.add(new ClassMethodCount(className, methodCount)); + totalClasses++; + totalMethods += methodCount; + } catch (NoClassDefFoundError e) { + // ignore + } catch (ClassNotFoundException e) { + // ignore + } + } + } + } + + ourLog.info("File {} contains {} entries", file, names.size()); + ourLog.info("Total classes {} - Total methods {}", totalClasses, totalMethods); + ourLog.info("Top classes {}", new ArrayList(topMethods).subList(Math.max(0, topMethods.size() - 10), topMethods.size())); + + } finally { + zip.close(); + } + } + } + + private static class ClassMethodCount implements Comparable { + + private String myClassName; + private int myMethodCount; + + public ClassMethodCount(String theClassName, int theMethodCount) { + myClassName = theClassName; + myMethodCount = theMethodCount; + } + + @Override + public String toString() { + return myClassName + "[" + myMethodCount + "]"; + } + + @Override + public int compareTo(ClassMethodCount theO) { + return myMethodCount - theO.myMethodCount; + } + + public String getClassName() { + return myClassName; + } + + public void setClassName(String theClassName) { + myClassName = theClassName; + } + + public int getMethodCount() { + return myMethodCount; + } + + public void setMethodCount(int theMethodCount) { + myMethodCount = theMethodCount; + } + + } + +} diff --git a/hapi-fhir-android/src/test/java/ca/uhn/fhir/android/BuiltJarIT.java b/hapi-fhir-android/src/test/java/ca/uhn/fhir/android/BuiltJarDstu2ShadeIT.java similarity index 85% rename from hapi-fhir-android/src/test/java/ca/uhn/fhir/android/BuiltJarIT.java rename to hapi-fhir-android/src/test/java/ca/uhn/fhir/android/BuiltJarDstu2ShadeIT.java index f14c6534298..89776450c80 100644 --- a/hapi-fhir-android/src/test/java/ca/uhn/fhir/android/BuiltJarIT.java +++ b/hapi-fhir-android/src/test/java/ca/uhn/fhir/android/BuiltJarDstu2ShadeIT.java @@ -19,52 +19,41 @@ import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.rest.client.IGenericClient; import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; -public class BuiltJarIT { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BuiltJarIT.class); +public class BuiltJarDstu2ShadeIT { + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BuiltJarDstu2ShadeIT.class); - @BeforeClass - public static void beforeClass() { - System.setProperty("javax.xml.stream.XMLInputFactory", "com.ctc.wstx.stax.WstxInputFactory"); - System.setProperty("javax.xml.stream.XMLOutputFactory", "com.ctc.wstx.stax.WstxOutputFactory"); - } - @Test public void testParserXml() throws Exception { - try { - Class.forName("com.ctc.wstx.stax.WstxOutputFactory"); - } catch (ClassNotFoundException e) { - return; - } - + FhirContext ctx = FhirContext.forDstu2(); - + Patient p = new Patient(); p.addIdentifier().setSystem("system"); - + String str = ctx.newXmlParser().encodeResourceToString(p); Patient p2 = ctx.newXmlParser().parseResource(Patient.class, str); - + assertEquals("system", p2.getIdentifierFirstRep().getSystemElement().getValueAsString()); } - + @Test public void testParserJson() { - + FhirContext ctx = FhirContext.forDstu2(); - + Observation o = new Observation(); o.getCode().setText("TEXT"); o.setValue(new QuantityDt(123)); o.addIdentifier().setSystem("system"); - + String str = ctx.newJsonParser().encodeResourceToString(o); Observation p2 = ctx.newJsonParser().parseResource(Observation.class, str); - + assertEquals("TEXT", p2.getCode().getText()); - + QuantityDt dt = (QuantityDt) p2.getValue(); dt.getComparatorElement().getValueAsEnum(); - + } /** @@ -83,7 +72,7 @@ public class BuiltJarIT { // this is good } } - + /** * Android does not like duplicate entries in the JAR */ @@ -105,15 +94,15 @@ public class BuiltJarIT { if (file.getName().contains("original.jar")) { continue; } - + ourLog.info("Testing file: {}", file); ZipFile zip = new ZipFile(file); - + int totalClasses = 0; int totalMethods = 0; TreeSet topMethods = new TreeSet(); - + try { Set names = new HashSet(); for (Enumeration iter = zip.entries(); iter.hasMoreElements();) { @@ -122,16 +111,16 @@ public class BuiltJarIT { if (!names.add(nextName)) { throw new Exception("File " + file + " contains duplicate contents: " + nextName); } - + if (nextName.contains("$") == false) { if (nextName.endsWith(".class")) { String className = nextName.replace("/", ".").replace(".class", ""); try { - Class clazz = Class.forName(className); - int methodCount = clazz.getMethods().length; - topMethods.add(new ClassMethodCount(className, methodCount)); - totalClasses++; - totalMethods += methodCount; + Class clazz = Class.forName(className); + int methodCount = clazz.getMethods().length; + topMethods.add(new ClassMethodCount(className, methodCount)); + totalClasses++; + totalMethods += methodCount; } catch (NoClassDefFoundError e) { // ignore } catch (ClassNotFoundException e) { @@ -140,11 +129,11 @@ public class BuiltJarIT { } } } - + ourLog.info("File {} contains {} entries", file, names.size()); ourLog.info("Total classes {} - Total methods {}", totalClasses, totalMethods); - ourLog.info("Top classes {}", new ArrayList(topMethods).subList(Math.max(0,topMethods.size() - 10), topMethods.size())); - + ourLog.info("Top classes {}", new ArrayList(topMethods).subList(Math.max(0, topMethods.size() - 10), topMethods.size())); + } finally { zip.close(); } @@ -155,7 +144,7 @@ public class BuiltJarIT { private String myClassName; private int myMethodCount; - + public ClassMethodCount(String theClassName, int theMethodCount) { myClassName = theClassName; myMethodCount = theMethodCount; @@ -186,7 +175,7 @@ public class BuiltJarIT { public void setMethodCount(int theMethodCount) { myMethodCount = theMethodCount; } - + } - + } diff --git a/hapi-fhir-android/src/test/java/ca/uhn/fhir/android/client/GenericClientDstu3IT.java b/hapi-fhir-android/src/test/java/ca/uhn/fhir/android/client/GenericClientDstu3IT.java new file mode 100644 index 00000000000..c44b2934917 --- /dev/null +++ b/hapi-fhir-android/src/test/java/ca/uhn/fhir/android/client/GenericClientDstu3IT.java @@ -0,0 +1,996 @@ +package ca.uhn.fhir.android.client; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.startsWith; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.ReaderInputStream; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +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.BasicStatusLine; +import org.hl7.fhir.dstu3.model.*; +import org.hl7.fhir.dstu3.model.Bundle.BundleType; +import org.junit.*; +import org.mockito.ArgumentCaptor; +import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.model.api.TemporalPrecisionEnum; +import ca.uhn.fhir.model.primitive.DateTimeDt; +import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.model.primitive.UriDt; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.PreferReturnEnum; +import ca.uhn.fhir.rest.client.IGenericClient; +import ca.uhn.fhir.rest.client.ServerValidationModeEnum; +import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; +import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; +import ca.uhn.fhir.rest.client.interceptor.UserInfoInterceptor; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; +import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException; +import ca.uhn.fhir.util.TestUtil; +import ca.uhn.fhir.util.UrlUtil; +import ca.uhn.fhir.util.VersionUtil; + +public class GenericClientDstu3IT { + + + private static FhirContext ourCtx; + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(GenericClientDstu3IT.class); + private int myAnswerCount; + private HttpClient myHttpClient; + private HttpResponse myHttpResponse; + + @Before + public void before() { + myHttpClient = mock(HttpClient.class, new ReturnsDeepStubs()); + ourCtx.getRestfulClientFactory().setHttpClient(myHttpClient); + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); + myAnswerCount = 0; + + } + + private String expectedUserAgent() { + return "HAPI-FHIR/" + VersionUtil.getVersion() + " (FHIR Client; FHIR " + FhirVersionEnum.DSTU3.getFhirVersionString() + "/DSTU3; apache)"; + } + + + private String extractBodyAsString(ArgumentCaptor capt) throws IOException { + String body = IOUtils.toString(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(0)).getEntity().getContent(), "UTF-8"); + return body; + } + + /** + * TODO: narratives don't work without stax + */ + @Test + @Ignore + public void testBinaryCreateWithFhirContentType() throws Exception { + IParser p = ourCtx.newXmlParser(); + + OperationOutcome conf = new OperationOutcome(); + conf.getText().setDivAsString("OK!"); + + final String respString = p.encodeResourceToString(conf); + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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 ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient pt = new Patient(); + pt.getText().setDivAsString("A PATIENT"); + + Binary bin = new Binary(); + bin.setContent(ourCtx.newJsonParser().encodeResourceToString(pt).getBytes("UTF-8")); + bin.setContentType(Constants.CT_FHIR_JSON); + client.create().resource(bin).execute(); + + ourLog.info(Arrays.asList(capt.getAllValues().get(0).getAllHeaders()).toString()); + + assertEquals("http://example.com/fhir/Binary", capt.getAllValues().get(0).getURI().toASCIIString()); + validateUserAgent(capt); + + assertEquals("application/xml+fhir;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", "")); + assertEquals(Constants.CT_FHIR_XML, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue()); + Binary output = ourCtx.newXmlParser().parseResource(Binary.class, extractBodyAsString(capt)); + assertEquals(Constants.CT_FHIR_JSON, output.getContentType()); + + Patient outputPt = (Patient) ourCtx.newJsonParser().parseResource(new String(output.getContent(), "UTF-8")); + assertEquals("
A PATIENT
", outputPt.getText().getDivAsString()); + } + + /** + * See #150 + */ + @Test + public void testNullAndEmptyParamValuesAreIgnored() throws Exception { + ArgumentCaptor capt = prepareClientForSearchResponse(); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + int idx = 0; + + //@formatter:off + client + .search() + .forResource(Patient.class) + .where(Patient.FAMILY.matches().value((String)null)) + .and(Patient.BIRTHDATE.exactly().day((Date)null)) + .and(Patient.GENDER.exactly().code((String)null)) + .and(Patient.ORGANIZATION.hasId((String)null)) + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + + assertEquals("http://example.com/fhir/Patient?_format=json", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + } + + + + /** + * TODO: narratives don't work without stax + */ + @Test + @Ignore + public void testBinaryCreateWithNoContentType() throws Exception { + IParser p = ourCtx.newJsonParser(); + + OperationOutcome conf = new OperationOutcome(); + conf.getText().setDivAsString("OK!"); + + final String respString = p.encodeResourceToString(conf); + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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_JSON_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Binary bin = new Binary(); + bin.setContent(new byte[] { 0, 1, 2, 3, 4 }); + client.create().resource(bin).execute(); + + ourLog.info(Arrays.asList(capt.getAllValues().get(0).getAllHeaders()).toString()); + + assertEquals("http://example.com/fhir/Binary", capt.getAllValues().get(0).getURI().toASCIIString()); + validateUserAgent(capt); + + assertEquals("application/xml+fhir;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", "")); + assertEquals(Constants.CT_FHIR_XML, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue()); + assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourCtx.newXmlParser().parseResource(Binary.class, extractBodyAsString(capt)).getContent()); + + } + + @SuppressWarnings("unchecked") + @Test + public void testClientFailures() throws Exception { + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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()).thenThrow(IllegalStateException.class, RuntimeException.class, Exception.class); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + try { + client.read().resource(Patient.class).withId("1").execute(); + fail(); + } catch (FhirClientConnectionException e) { + assertEquals("java.lang.IllegalStateException", e.getMessage()); + } + + try { + client.read().resource(Patient.class).withId("1").execute(); + fail(); + } catch (RuntimeException e) { + assertEquals("java.lang.RuntimeException", e.toString()); + } + + try { + client.read().resource(Patient.class).withId("1").execute(); + fail(); + } catch (FhirClientConnectionException e) { + assertEquals("java.lang.Exception", e.getMessage()); + } + } + + + + /** + * TODO: narratives don't work without stax + */ + @Test + @Ignore + public void testCreateWithPreferRepresentationServerReturnsResource() throws Exception { + final IParser p = ourCtx.newJsonParser(); + + final Patient resp1 = new Patient(); + resp1.getText().setDivAsString("FINAL VALUE"); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer() { + @Override + public Header[] answer(InvocationOnMock theInvocation) throws Throwable { + return new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3") }; + } + }); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + myAnswerCount++; + return new ReaderInputStream(new StringReader(p.encodeResourceToString(resp1)), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient pt = new Patient(); + pt.getText().setDivAsString("A PATIENT"); + + MethodOutcome outcome = client.create().resource(pt).prefer(PreferReturnEnum.REPRESENTATION).execute(); + + assertEquals(1, myAnswerCount); + assertNull(outcome.getOperationOutcome()); + assertNotNull(outcome.getResource()); + + assertEquals("
FINAL VALUE
", ((Patient) outcome.getResource()).getText().getDivAsString()); + + assertEquals(myAnswerCount, capt.getAllValues().size()); + assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(0).getURI().toASCIIString()); + } + + + + + + + + @Test + public void testForceConformance() throws Exception { + final IParser p = ourCtx.newJsonParser(); + + final Conformance conf = new Conformance(); + conf.setCopyright("COPY"); + + final Patient patient = new Patient(); + patient.addName().addFamily("FAM"); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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_JSON_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + private int myCount = 0; + + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + final String respString; + if (myCount == 1 || myCount == 2) { + ourLog.info("Encoding patient"); + respString = p.encodeResourceToString(patient); + } else { + ourLog.info("Encoding conformance"); + respString = p.encodeResourceToString(conf); + } + myCount++; + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.ONCE); + IGenericClient client = ourCtx.newRestfulGenericClient("http://testForceConformance.com/fhir"); + + client.read().resource("Patient").withId("1").execute(); + assertEquals(2, capt.getAllValues().size()); + assertEquals("http://testForceConformance.com/fhir/metadata?_format=json", capt.getAllValues().get(0).getURI().toASCIIString()); + assertEquals("http://testForceConformance.com/fhir/Patient/1?_format=json", capt.getAllValues().get(1).getURI().toASCIIString()); + + client.read().resource("Patient").withId("1").execute(); + assertEquals(3, capt.getAllValues().size()); + assertEquals("http://testForceConformance.com/fhir/Patient/1?_format=json", capt.getAllValues().get(2).getURI().toASCIIString()); + + client.forceConformanceCheck(); + assertEquals(4, capt.getAllValues().size()); + assertEquals("http://testForceConformance.com/fhir/metadata?_format=json", capt.getAllValues().get(3).getURI().toASCIIString()); + } + + @Test + public void testHttp499() throws Exception { + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 499, "Wacky Message")); + 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 { + return new ReaderInputStream(new StringReader("HELLO"), StandardCharsets.UTF_8); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + try { + client.read().resource(Patient.class).withId("1").execute(); + fail(); + } catch (UnclassifiedServerFailureException e) { + assertEquals("ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException: HTTP 499 Wacky Message", e.toString()); + assertEquals("HELLO", e.getResponseBody()); + } + + } + + @Test + public void testHttp501() throws Exception { + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 501, "Not Implemented")); + 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 { + return new ReaderInputStream(new StringReader("not implemented"), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + try { + client.read().resource(Patient.class).withId("1").execute(); + fail(); + } catch (NotImplementedOperationException e) { + assertEquals("HTTP 501 Not Implemented", e.getMessage()); + } + + } + + @SuppressWarnings("deprecation") + @Test + public void testInvalidConformanceCall() { + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + try { + client.conformance(); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Must call fetchConformance() instead of conformance() for RI/STU3+ structures", e.getMessage()); + } + } + + + + @Test + public void testPutDoesntForceAllIdsJson() throws Exception { + IParser p = ourCtx.newJsonParser(); + + Patient patient = new Patient(); + patient.setId("PATIENT1"); + patient.addName().addFamily("PATIENT1"); + + Bundle bundle = new Bundle(); + bundle.setId("BUNDLE1"); + bundle.addEntry().setResource(patient); + + final String encoded = p.encodeResourceToString(bundle); + assertEquals("{\"resourceType\":\"Bundle\",\"id\":\"BUNDLE1\",\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"PATIENT1\",\"name\":[{\"family\":[\"PATIENT1\"]}]}}]}", encoded); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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_JSON_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(encoded), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + //@formatter:off + client + .update() + .resource(bundle) + .prefer(PreferReturnEnum.REPRESENTATION) + .encodedJson() + .execute(); + //@formatter:on + + HttpPut httpRequest = (HttpPut) capt.getValue(); + assertEquals("http://example.com/fhir/Bundle/BUNDLE1?_format=json", httpRequest.getURI().toASCIIString()); + + String requestString = IOUtils.toString(httpRequest.getEntity().getContent(), StandardCharsets.UTF_8); + assertEquals(encoded, requestString); + } + + @Test + public void testResponseHasContentTypeMissing() throws Exception { + IParser p = ourCtx.newJsonParser(); + Patient patient = new Patient(); + patient.addName().addFamily("FAM"); + final String respString = p.encodeResourceToString(patient); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(null); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + try { + client.read().resource(Patient.class).withId("1").execute(); + fail(); + } catch (NonFhirResponseException e) { + assertEquals("Response contains no Content-Type", e.getMessage()); + } + + // Patient resp = client.read().resource(Patient.class).withId("1").execute(); + // assertEquals("FAM", resp.getNameFirstRep().getFamilyAsSingleString()); + } + + @Test + public void testResponseHasContentTypeNonFhir() throws Exception { + IParser p = ourCtx.newJsonParser(); + Patient patient = new Patient(); + patient.addName().addFamily("FAM"); + final String respString = p.encodeResourceToString(patient); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", "text/plain")); + // when(myHttpResponse.getEntity().getContentType()).thenReturn(null); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + try { + client.read().resource(Patient.class).withId("1").execute(); + fail(); + } catch (NonFhirResponseException e) { + assertEquals("Response contains non FHIR Content-Type 'text/plain' : {\"resourceType\":\"Patient\",\"name\":[{\"family\":[\"FAM\"]}]}", e.getMessage()); + } + + // Patient resp = client.read().resource(Patient.class).withId("1").execute(); + // assertEquals("FAM", resp.getNameFirstRep().getFamilyAsSingleString()); + } + + @Test + public void testSearchByDate() throws Exception { + final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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_JSON + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).then(new Answer() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + int idx = 0; + + DateTimeDt now = DateTimeDt.withCurrentTime(); + String dateString = now.getValueAsString().substring(0, 10); + + //@formatter:off + client.search() + .forResource("Patient") + .where(Patient.BIRTHDATE.after().day(dateString)) + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Patient?birthdate=gt"+dateString + "&_format=json", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + } + + + @Test + public void testSearchByString() throws Exception { + final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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_JSON + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).then(new Answer() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + int idx = 0; + + //@formatter:off + client.search() + .forResource("Patient") + .where(Patient.NAME.matches().value("AAA")) + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Patient?name=AAA&_format=json", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + //@formatter:off + client.search() + .forResource("Patient") + .where(Patient.NAME.matches().value(new StringDt("AAA"))) + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Patient?name=AAA&_format=json", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + //@formatter:off + client.search() + .forResource("Patient") + .where(Patient.NAME.matches().values("AAA", "BBB")) + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Patient?name=AAA,BBB&_format=json", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); + idx++; + + //@formatter:off + client.search() + .forResource("Patient") + .where(Patient.NAME.matches().values(Arrays.asList("AAA", "BBB"))) + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Patient?name=AAA,BBB&_format=json", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); + idx++; + + //@formatter:off + client.search() + .forResource("Patient") + .where(Patient.NAME.matchesExactly().value("AAA")) + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Patient?name%3Aexact=AAA&_format=json", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + //@formatter:off + client.search() + .forResource("Patient") + .where(Patient.NAME.matchesExactly().value(new StringDt("AAA"))) + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Patient?name%3Aexact=AAA&_format=json", capt.getAllValues().get(idx).getURI().toString()); + idx++; + + } + + @Test + public void testSearchByUrl() throws Exception { + final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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_JSON + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).then(new Answer() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + int idx = 0; + + //@formatter:off + client.search() + .forResource("Device") + .where(Device.URL.matches().value("http://foo.com")) + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Device?url=http://foo.com&_format=json", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); + idx++; + + } + + @Test + public void testAcceptHeaderWithEncodingSpecified() throws Exception { + final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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_JSON + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).then(new Answer() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + int idx = 0; + + //@formatter:off + client.setEncoding(EncodingEnum.JSON); + client.search() + .forResource("Device") + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Device?_format=json", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); + assertEquals("application/fhir+json;q=1.0, application/json+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue()); + idx++; + + //@formatter:off + client.setEncoding(EncodingEnum.XML); + client.search() + .forResource("Device") + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Device?_format=xml", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); + assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue()); + idx++; + + } + + @Test + public void testSearchForUnknownType() throws Exception { + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + try { + client.search(new UriDt("?aaaa")); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Unable to determine the resource type from the given URI: ?aaaa", e.getMessage()); + } + } + + private ArgumentCaptor prepareClientForSearchResponse() throws IOException, ClientProtocolException { + final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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_JSON + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).then(new Answer() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); + } + }); + return capt; + } + + + + @Test + public void testTransactionWithInvalidBody() throws Exception { + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + // Transaction + try { + client.transaction().withBundle("FOO"); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Unable to determing encoding of request (body does not appear to be valid XML or JSON)", e.getMessage()); + } + + // Create + try { + client.create().resource("FOO").execute(); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Unable to determing encoding of request (body does not appear to be valid XML or JSON)", e.getMessage()); + } + + // Update + try { + client.update().resource("FOO").execute(); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Unable to determing encoding of request (body does not appear to be valid XML or JSON)", e.getMessage()); + } + + // Validate + try { + client.validate().resource("FOO").execute(); + fail(); + } catch (IllegalArgumentException e) { + assertEquals("Unable to determing encoding of request (body does not appear to be valid XML or JSON)", e.getMessage()); + } + + + } + + /** + * TODO: narratives don't work without stax + */ + @Test + @Ignore + public void testUpdateById() throws Exception { + IParser p = ourCtx.newJsonParser(); + + OperationOutcome conf = new OperationOutcome(); + conf.getText().setDivAsString("OK!"); + + final String respString = p.encodeResourceToString(conf); + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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_JSON_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient pt = new Patient(); + pt.setId("222"); + pt.getText().setDivAsString("A PATIENT"); + + client.update().resource(pt).withId("111").execute(); + + ourLog.info(Arrays.asList(capt.getAllValues().get(0).getAllHeaders()).toString()); + + assertEquals("http://example.com/fhir/Patient/111", capt.getAllValues().get(0).getURI().toASCIIString()); + validateUserAgent(capt); + + assertEquals("application/xml+fhir;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", "")); + assertEquals(Constants.HEADER_ACCEPT_VALUE_JSON_NON_LEGACY, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue()); + String body = extractBodyAsString(capt); + assertThat(body, containsString("")); + } + + /** + * TODO: narratives don't work without stax + */ + @Test + @Ignore + public void testUpdateWithPreferRepresentationServerReturnsOO() throws Exception { + final IParser p = ourCtx.newJsonParser(); + + final OperationOutcome resp0 = new OperationOutcome(); + resp0.getText().setDivAsString("OK!"); + + final Patient resp1 = new Patient(); + resp1.getText().setDivAsString("FINAL VALUE"); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer() { + @Override + public Header[] answer(InvocationOnMock theInvocation) throws Throwable { + return new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3") }; + } + }); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + if (myAnswerCount++ == 0) { + return new ReaderInputStream(new StringReader(p.encodeResourceToString(resp0)), Charset.forName("UTF-8")); + } else { + return new ReaderInputStream(new StringReader(p.encodeResourceToString(resp1)), Charset.forName("UTF-8")); + } + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient pt = new Patient(); + pt.setId("Patient/222"); + pt.getText().setDivAsString("A PATIENT"); + + MethodOutcome outcome = client.update().resource(pt).prefer(PreferReturnEnum.REPRESENTATION).execute(); + + assertEquals(2, myAnswerCount); + assertNotNull(outcome.getOperationOutcome()); + assertNotNull(outcome.getResource()); + + assertEquals("
OK!
", ((OperationOutcome) outcome.getOperationOutcome()).getText().getDivAsString()); + assertEquals("
FINAL VALUE
", ((Patient) outcome.getResource()).getText().getDivAsString()); + + assertEquals(myAnswerCount, capt.getAllValues().size()); + assertEquals("http://example.com/fhir/Patient/222", capt.getAllValues().get(0).getURI().toASCIIString()); + assertEquals("http://foo.com/base/Patient/222/_history/3", capt.getAllValues().get(1).getURI().toASCIIString()); + } + + @Test + public void testUpdateWithPreferRepresentationServerReturnsResource() throws Exception { + final IParser p = ourCtx.newJsonParser(); + + final Patient resp1 = new Patient(); + resp1.setActive(true); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer() { + @Override + public Header[] answer(InvocationOnMock theInvocation) throws Throwable { + return new Header[] { new BasicHeader(Constants.HEADER_LOCATION, "http://foo.com/base/Patient/222/_history/3") }; + } + }); + when(myHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_JSON_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + myAnswerCount++; + return new ReaderInputStream(new StringReader(p.encodeResourceToString(resp1)), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient pt = new Patient(); + pt.setId("Patient/222"); + pt.getText().setDivAsString("A PATIENT"); + + MethodOutcome outcome = client.update().resource(pt).prefer(PreferReturnEnum.REPRESENTATION).execute(); + + assertEquals(1, myAnswerCount); + assertNull(outcome.getOperationOutcome()); + assertNotNull(outcome.getResource()); + + assertEquals(true, ((Patient) outcome.getResource()).getActive()); + + assertEquals(myAnswerCount, capt.getAllValues().size()); + assertEquals("http://example.com/fhir/Patient/222?_format=json", capt.getAllValues().get(0).getURI().toASCIIString()); + } + + + @Test + public void testUserAgentForConformance() throws Exception { + IParser p = ourCtx.newJsonParser(); + + Conformance conf = new Conformance(); + conf.setCopyright("COPY"); + + final String respString = p.encodeResourceToString(conf); + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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_JSON_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(respString), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + client.fetchConformance().ofType(Conformance.class).execute(); + assertEquals("http://example.com/fhir/metadata?_format=json", capt.getAllValues().get(0).getURI().toASCIIString()); + validateUserAgent(capt); + } + + + /** + * TODO: narratives don't work without stax + */ + @Test + @Ignore + public void testValidate() throws Exception { + final IParser p = ourCtx.newXmlParser(); + + final OperationOutcome resp0 = new OperationOutcome(); + resp0.getText().setDivAsString("OK!"); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + when(myHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(myHttpResponse.getAllHeaders()).thenAnswer(new Answer() { + @Override + public Header[] answer(InvocationOnMock theInvocation) throws Throwable { + return new Header[] {}; + } + }); + 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 ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(p.encodeResourceToString(resp0)), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient pt = new Patient(); + pt.setId("Patient/222"); + pt.getText().setDivAsString("A PATIENT"); + + MethodOutcome outcome = client.validate().resource(pt).execute(); + + assertNotNull(outcome.getOperationOutcome()); + assertEquals("
OK!
", ((OperationOutcome) outcome.getOperationOutcome()).getText().getDivAsString()); + + } + + private void validateUserAgent(ArgumentCaptor capt) { + assertEquals(1, capt.getAllValues().get(0).getHeaders("User-Agent").length); + assertEquals(expectedUserAgent(), capt.getAllValues().get(0).getHeaders("User-Agent")[0].getValue()); + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() { + + // Force StAX to fail like it will on android + System.setProperty("javax.xml.stream.XMLInputFactory", "FOO"); + System.setProperty("javax.xml.stream.XMLOutputFactory", "FOO"); + + ourCtx = FhirContext.forDstu3(); + } + +} diff --git a/hapi-fhir-base/.classpath b/hapi-fhir-base/.classpath index 3e3200180cf..d0b602b45e1 100644 --- a/hapi-fhir-base/.classpath +++ b/hapi-fhir-base/.classpath @@ -30,7 +30,7 @@ - + diff --git a/hapi-fhir-base/.settings/org.eclipse.jdt.core.prefs b/hapi-fhir-base/.settings/org.eclipse.jdt.core.prefs index 5e625f15742..201420a2537 100644 --- a/hapi-fhir-base/.settings/org.eclipse.jdt.core.prefs +++ b/hapi-fhir-base/.settings/org.eclipse.jdt.core.prefs @@ -6,13 +6,8 @@ org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annota org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.doc.comment.support=enabled org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning org.eclipse.jdt.core.compiler.problem.assertIdentifier=error diff --git a/hapi-fhir-base/pom.xml b/hapi-fhir-base/pom.xml index 2a4b0fb8de3..06c172a9617 100644 --- a/hapi-fhir-base/pom.xml +++ b/hapi-fhir-base/pom.xml @@ -20,9 +20,9 @@ - com.google.code.gson - gson - 2.7 + com.google.code.gson + gson + 2.7 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 0f2735fe022..8f84ff4f00e 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 @@ -29,69 +29,23 @@ import java.io.PushbackReader; import java.io.Reader; import java.io.Writer; import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.text.WordUtils; -import org.hl7.fhir.instance.model.api.IBase; -import org.hl7.fhir.instance.model.api.IBaseBinary; -import org.hl7.fhir.instance.model.api.IBaseBooleanDatatype; -import org.hl7.fhir.instance.model.api.IBaseDecimalDatatype; -import org.hl7.fhir.instance.model.api.IBaseExtension; -import org.hl7.fhir.instance.model.api.IBaseHasExtensions; -import org.hl7.fhir.instance.model.api.IBaseHasModifierExtensions; -import org.hl7.fhir.instance.model.api.IBaseIntegerDatatype; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IDomainResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.instance.model.api.INarrative; -import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.instance.model.api.*; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSyntaxException; +import com.google.gson.*; import com.google.gson.stream.JsonWriter; -import ca.uhn.fhir.context.BaseRuntimeChildDefinition; -import ca.uhn.fhir.context.BaseRuntimeElementDefinition; +import ca.uhn.fhir.context.*; import ca.uhn.fhir.context.BaseRuntimeElementDefinition.ChildTypeEnum; -import ca.uhn.fhir.context.ConfigurationException; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; -import ca.uhn.fhir.context.RuntimeChildContainedResources; -import ca.uhn.fhir.context.RuntimeChildDeclaredExtensionDefinition; -import ca.uhn.fhir.context.RuntimeChildNarrativeDefinition; -import ca.uhn.fhir.context.RuntimeChildUndeclaredExtensionDefinition; -import ca.uhn.fhir.context.RuntimeResourceDefinition; -import ca.uhn.fhir.model.api.BaseBundle; -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.api.IPrimitiveDatatype; -import ca.uhn.fhir.model.api.IResource; -import ca.uhn.fhir.model.api.ISupportsUndeclaredExtensions; -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.api.*; import ca.uhn.fhir.model.api.annotation.Child; import ca.uhn.fhir.model.base.composite.BaseCodingDt; import ca.uhn.fhir.model.base.composite.BaseContainedDt; -import ca.uhn.fhir.model.primitive.DecimalDt; -import ca.uhn.fhir.model.primitive.IdDt; -import ca.uhn.fhir.model.primitive.InstantDt; -import ca.uhn.fhir.model.primitive.IntegerDt; -import ca.uhn.fhir.model.primitive.StringDt; +import ca.uhn.fhir.model.primitive.*; import ca.uhn.fhir.narrative.INarrativeGenerator; import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.util.ElementUtil; @@ -170,6 +124,16 @@ public class JsonParser extends BaseParser implements IParser { } } + private void addToHeldIds(int theValueIdx, ArrayList theListToAddTo, String theId) { + theListToAddTo.ensureCapacity(theValueIdx); + while (theListToAddTo.size() <= theValueIdx) { + theListToAddTo.add(null); + } + if (theListToAddTo.get(theValueIdx) == null) { + theListToAddTo.set(theValueIdx, theId); + } + } + private void assertObjectOfType(JsonElement theResourceTypeObj, Object theValueType, String thePosition) { // if (theResourceTypeObj == null) { // throw new DataFormatException("Invalid JSON content detected, missing required element: '" + thePosition + "'"); @@ -180,6 +144,16 @@ public class JsonParser extends BaseParser implements IParser { // } } + private void beginArray(JsonWriter theEventWriter, String arrayName) throws IOException { + theEventWriter.name(arrayName); + theEventWriter.beginArray(); + } + + private void beginObject(JsonWriter theEventWriter, String arrayName) throws IOException { + theEventWriter.name(arrayName); + theEventWriter.beginObject(); + } + private JsonWriter createJsonWriter(Writer theWriter) { JsonWriter retVal = new JsonWriter(theWriter); retVal.setSerializeNulls(true); @@ -205,7 +179,7 @@ public class JsonParser extends BaseParser implements IParser { JsonWriter eventWriter = createJsonWriter(theWriter); RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theResource); - encodeResourceToJsonStreamWriter(resDef, theResource, eventWriter, null, false); + encodeResourceToJsonStreamWriter(resDef, theResource, eventWriter, null, false, false); eventWriter.flush(); } @@ -234,37 +208,6 @@ public class JsonParser extends BaseParser implements IParser { return retVal; } - private JsonObject parse(Reader theReader) { - - PushbackReader pbr = new PushbackReader(theReader); - JsonObject object; - try { - while(true) { - int nextInt; - nextInt = pbr.read(); - if (nextInt == -1) { - throw new DataFormatException("Did not find any content to parse"); - } - if (nextInt == '{') { - pbr.unread('{'); - break; - } - if (Character.isWhitespace(nextInt)) { - continue; - } - throw new DataFormatException("Content does not appear to be FHIR JSON, first non-whitespace character was: '" + (char)nextInt + "' (must be '{')"); - } - - Gson gson = new GsonBuilder().disableHtmlEscaping().create(); - - object = gson.fromJson(pbr, JsonObject.class); - } catch (Exception e) { - throw new DataFormatException("Failed to parse JSON content, error was: " + e.getMessage(), e); - } - - return object; - } - private void encodeBundleToWriterInDstu1Format(Bundle theBundle, JsonWriter theEventWriter) throws IOException { theEventWriter.beginObject(); @@ -320,7 +263,7 @@ public class JsonParser extends BaseParser implements IParser { IResource resource = nextEntry.getResource(); if (resource != null && !resource.isEmpty() && !deleted) { RuntimeResourceDefinition resDef = myContext.getResourceDefinition(resource); - encodeResourceToJsonStreamWriter(resDef, resource, theEventWriter, "content", false); + encodeResourceToJsonStreamWriter(resDef, resource, theEventWriter, "content", false, true); } if (nextEntry.getSummary().isEmpty() == false) { @@ -374,7 +317,7 @@ public class JsonParser extends BaseParser implements IParser { IResource resource = nextEntry.getResource(); if (resource != null && !resource.isEmpty() && !deleted) { RuntimeResourceDefinition resDef = myContext.getResourceDefinition(resource); - encodeResourceToJsonStreamWriter(resDef, resource, theEventWriter, "resource", false); + encodeResourceToJsonStreamWriter(resDef, resource, theEventWriter, "resource", false, true); } if (nextEntry.getSearchMode().isEmpty() == false || nextEntry.getScore().isEmpty() == false) { @@ -551,7 +494,7 @@ public class JsonParser extends BaseParser implements IParser { case RESOURCE: IBaseResource resource = (IBaseResource) theNextValue; RuntimeResourceDefinition def = myContext.getResourceDefinition(resource); - encodeResourceToJsonStreamWriter(def, resource, theEventWriter, theChildName, false); + encodeResourceToJsonStreamWriter(def, resource, theEventWriter, theChildName, false, true); break; case UNDECL_EXT: default: @@ -560,23 +503,6 @@ public class JsonParser extends BaseParser implements IParser { } - private void write(JsonWriter theEventWriter, String theChildName, Boolean theValue) throws IOException { - if (theValue != null) { - theEventWriter.name(theChildName); - theEventWriter.value(theValue.booleanValue()); - } - } - - private void write(JsonWriter theEventWriter, String theChildName, BigDecimal theDecimalValue) throws IOException { - theEventWriter.name(theChildName); - theEventWriter.value(theDecimalValue); - } - - private void write(JsonWriter theEventWriter, String theChildName, Integer theValue) throws IOException { - theEventWriter.name(theChildName); - theEventWriter.value(theValue); - } - private void encodeCompositeElementChildrenToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theElement, JsonWriter theEventWriter, boolean theContainedResource, CompositeChildElement theParent) throws IOException { { @@ -796,23 +722,13 @@ public class JsonParser extends BaseParser implements IParser { } } - private void addToHeldIds(int theValueIdx, ArrayList theListToAddTo, String theId) { - theListToAddTo.ensureCapacity(theValueIdx); - while (theListToAddTo.size() <= theValueIdx) { - theListToAddTo.add(null); - } - if (theListToAddTo.get(theValueIdx) == null) { - theListToAddTo.set(theValueIdx, theId); - } - } - private void encodeCompositeElementToStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, IBase theNextValue, JsonWriter theEventWriter, boolean theContainedResource, CompositeChildElement theParent) throws IOException, DataFormatException { writeCommentsPreAndPost(theNextValue, theEventWriter); encodeCompositeElementChildrenToStreamWriter(theResDef, theResource, theNextValue, theEventWriter, theContainedResource, theParent); } - private void encodeResourceToJsonStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonWriter theEventWriter, String theObjectNameOrNull, boolean theContainedResource) throws IOException { + private void encodeResourceToJsonStreamWriter(RuntimeResourceDefinition theResDef, IBaseResource theResource, JsonWriter theEventWriter, String theObjectNameOrNull, boolean theContainedResource, boolean theSubResource) throws IOException { IIdType resourceId = null; // if (theResource instanceof IResource) { // IResource res = (IResource) theResource; @@ -843,7 +759,7 @@ public class JsonParser extends BaseParser implements IParser { if (!theContainedResource) { if (super.shouldEncodeResourceId(theResource) == false) { resourceId = null; - } else if (getEncodeForceResourceId() != null) { + } else if (!theSubResource && getEncodeForceResourceId() != null) { resourceId = getEncodeForceResourceId(); } } @@ -1083,6 +999,37 @@ public class JsonParser extends BaseParser implements IParser { return (JsonArray) object; } + private JsonObject parse(Reader theReader) { + + PushbackReader pbr = new PushbackReader(theReader); + JsonObject object; + try { + while(true) { + int nextInt; + nextInt = pbr.read(); + if (nextInt == -1) { + throw new DataFormatException("Did not find any content to parse"); + } + if (nextInt == '{') { + pbr.unread('{'); + break; + } + if (Character.isWhitespace(nextInt)) { + continue; + } + throw new DataFormatException("Content does not appear to be FHIR JSON, first non-whitespace character was: '" + (char)nextInt + "' (must be '{')"); + } + + Gson gson = newGson(); + + object = gson.fromJson(pbr, JsonObject.class); + } catch (Exception e) { + throw new DataFormatException("Failed to parse JSON content, error was: " + e.getMessage(), e); + } + + return object; + } + private void parseAlternates(JsonElement theAlternateVal, ParserState theState, String theElementName) { if (theAlternateVal == null || theAlternateVal instanceof JsonNull) { return; @@ -1404,6 +1351,17 @@ public class JsonParser extends BaseParser implements IParser { return state.getObject(); } + @Override + public IParser setPrettyPrint(boolean thePrettyPrint) { + myPrettyPrint = thePrettyPrint; + return this; + } + + private void write(JsonWriter theEventWriter, String theChildName, BigDecimal theDecimalValue) throws IOException { + theEventWriter.name(theChildName); + theEventWriter.value(theDecimalValue); + } + // private void parseExtensionInDstu2Style(boolean theModifier, ParserState theState, String // theParentExtensionUrl, String theExtensionUrl, JsonArray theValues) { // String extUrl = UrlUtil.constructAbsoluteUrl(theParentExtensionUrl, theExtensionUrl); @@ -1430,10 +1388,16 @@ public class JsonParser extends BaseParser implements IParser { // theState.endingElement(); // } - @Override - public IParser setPrettyPrint(boolean thePrettyPrint) { - myPrettyPrint = thePrettyPrint; - return this; + private void write(JsonWriter theEventWriter, String theChildName, Boolean theValue) throws IOException { + if (theValue != null) { + theEventWriter.name(theChildName); + theEventWriter.value(theValue.booleanValue()); + } + } + + private void write(JsonWriter theEventWriter, String theChildName, Integer theValue) throws IOException { + theEventWriter.name(theChildName); + theEventWriter.value(theValue); } private boolean writeAtomLinkInDstu1Format(JsonWriter theEventWriter, String theRel, StringDt theLink, boolean theStarted) throws IOException { @@ -1481,16 +1445,6 @@ public class JsonParser extends BaseParser implements IParser { } } - private void beginArray(JsonWriter theEventWriter, String arrayName) throws IOException { - theEventWriter.name(arrayName); - theEventWriter.beginArray(); - } - - private void beginObject(JsonWriter theEventWriter, String arrayName) throws IOException { - theEventWriter.name(arrayName); - theEventWriter.beginObject(); - } - private void writeCategories(JsonWriter theEventWriter, TagList categories) throws IOException { if (categories != null && categories.size() > 0) { theEventWriter.name("category"); @@ -1586,13 +1540,23 @@ public class JsonParser extends BaseParser implements IParser { // } } + public static Gson newGson() { + Gson gson = new GsonBuilder().disableHtmlEscaping().create(); + return gson; + } + + private static void write(JsonWriter theWriter, String theName, String theValue) throws IOException { + theWriter.name(theName); + theWriter.value(theValue); + } + private class HeldExtension implements Comparable { + private CompositeChildElement myChildElem; private RuntimeChildDeclaredExtensionDefinition myDef; private boolean myModifier; private IBaseExtension myUndeclaredExtension; private IBase myValue; - private CompositeChildElement myChildElem; public HeldExtension(IBaseExtension theUndeclaredExtension, boolean theModifier, CompositeChildElement theChildElem) { assert theUndeclaredExtension != null; @@ -1697,10 +1661,5 @@ public class JsonParser extends BaseParser implements IParser { } } - - private static void write(JsonWriter theWriter, String theName, String theValue) throws IOException { - theWriter.name(theName); - theWriter.value(theValue); - } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java index 42ce4a4677a..1cc7ed4fbdb 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java @@ -25,6 +25,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.isNotEmpty; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -1870,13 +1871,17 @@ class ParserState { } else if (theLocalPart.equals("profile")) { @SuppressWarnings("unchecked") List profiles = (List) myMap.get(ResourceMetadataKeyEnum.PROFILES); - if (profiles == null) { - profiles = new ArrayList(); - myMap.put(ResourceMetadataKeyEnum.PROFILES, profiles); + List newProfiles; + if (profiles != null) { + newProfiles = new ArrayList(profiles.size() + 1); + newProfiles.addAll(profiles); + } else { + newProfiles = new ArrayList(1); } IdDt profile = new IdDt(); push(new PrimitiveState(getPreResourceState(), profile)); - profiles.add(profile); + newProfiles.add(profile); + myMap.put(ResourceMetadataKeyEnum.PROFILES, Collections.unmodifiableList(newProfiles)); } else if (theLocalPart.equals("tag")) { TagList tagList = (TagList) myMap.get(ResourceMetadataKeyEnum.TAG_LIST); if (tagList == null) { 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 0be6e2fc2e3..2468f079646 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 @@ -174,7 +174,7 @@ public class XmlParser extends BaseParser implements IParser { try { eventWriter = createXmlWriter(theWriter); - encodeResourceToXmlStreamWriter(theResource, eventWriter, false); + encodeResourceToXmlStreamWriter(theResource, eventWriter, false, false); eventWriter.flush(); } catch (XMLStreamException e) { throw new ConfigurationException("Failed to initialize STaX event factory", e); @@ -384,7 +384,7 @@ public class XmlParser extends BaseParser implements IParser { if (resource != null && !resource.isEmpty() && !deleted) { eventWriter.writeStartElement("content"); eventWriter.writeAttribute("type", "text/xml"); - encodeResourceToXmlStreamWriter(resource, eventWriter, false); + encodeResourceToXmlStreamWriter(resource, eventWriter, false, true); eventWriter.writeEndElement(); // content } else { ourLog.debug("Bundle entry contains null resource"); @@ -447,7 +447,7 @@ public class XmlParser extends BaseParser implements IParser { IResource resource = nextEntry.getResource(); if (resource != null && !resource.isEmpty() && !deleted) { theEventWriter.writeStartElement("resource"); - encodeResourceToXmlStreamWriter(resource, theEventWriter, false); + encodeResourceToXmlStreamWriter(resource, theEventWriter, false, true); theEventWriter.writeEndElement(); // content } else { ourLog.debug("Bundle entry contains null resource"); @@ -557,7 +557,7 @@ public class XmlParser extends BaseParser implements IParser { case RESOURCE: { theEventWriter.writeStartElement(childName); IBaseResource resource = (IBaseResource) theElement; - encodeResourceToXmlStreamWriter(resource, theEventWriter, false); + encodeResourceToXmlStreamWriter(resource, theEventWriter, false, true); theEventWriter.writeEndElement(); break; } @@ -711,7 +711,7 @@ public class XmlParser extends BaseParser implements IParser { } } - private void encodeResourceToXmlStreamWriter(IBaseResource theResource, XMLStreamWriter theEventWriter, boolean theIncludedResource) throws XMLStreamException, DataFormatException { + private void encodeResourceToXmlStreamWriter(IBaseResource theResource, XMLStreamWriter theEventWriter, boolean theIncludedResource, boolean theSubResource) throws XMLStreamException, DataFormatException { IIdType resourceId = null; if (StringUtils.isNotBlank(theResource.getIdElement().getIdPart())) { @@ -727,7 +727,7 @@ public class XmlParser extends BaseParser implements IParser { if (!theIncludedResource) { if (super.shouldEncodeResourceId(theResource) == false) { resourceId = null; - } else if (getEncodeForceResourceId() != null) { + } else if (theSubResource == false && getEncodeForceResourceId() != null) { resourceId = getEncodeForceResourceId(); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Delete.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Delete.java index 1fb58e06f75..db64d821d6f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Delete.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Delete.java @@ -25,6 +25,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.hl7.fhir.instance.model.api.IBaseResource; + import ca.uhn.fhir.model.api.IResource; /** @@ -47,5 +49,5 @@ public @interface Delete { * for client implementations. */ // NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere - Class type() default IResource.class; + Class type() default IBaseResource.class; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Patch.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Patch.java new file mode 100644 index 00000000000..50efd6a6089 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/annotation/Patch.java @@ -0,0 +1,53 @@ +package ca.uhn.fhir.rest.annotation; + +import java.lang.annotation.ElementType; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +/** + * RESTful method annotation to be used for the proposed FHIR + * PATCH method + * + *

+ * Patch is used to apply a differential to a resource in either + * XML or JSON format + *

+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD }) +public @interface Patch { + + /** + * The return type for this search method. This generally does not need + * to be populated for a server implementation, since servers will return + * only one resource per class, but generally does need to be populated + * for client implementations. + */ + // NB: Read, Search (maybe others) share this annotation, so update the javadocs everywhere + Class type() default IBaseResource.class; + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PatchTypeEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PatchTypeEnum.java new file mode 100644 index 00000000000..24fbbf48107 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/PatchTypeEnum.java @@ -0,0 +1,43 @@ +package ca.uhn.fhir.rest.api; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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.annotation.Patch; +import ca.uhn.fhir.rest.server.Constants; + +/** + * Parameter type for methods annotated with {@link Patch} + */ +public enum PatchTypeEnum { + + JSON_PATCH(Constants.CT_JSON_PATCH), XML_PATCH(Constants.CT_XML_PATCH); + + private final String myContentType; + + PatchTypeEnum(String theContentType) { + myContentType = theContentType; + } + + public String getContentType() { + return myContentType; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RestOperationTypeEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RestOperationTypeEnum.java index 5e62a58a7e0..e6ccf32768c 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RestOperationTypeEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RestOperationTypeEnum.java @@ -130,7 +130,12 @@ public enum RestOperationTypeEnum { /** * $meta-delete extended operation */ - META_DELETE("$meta-delete"), + META_DELETE("$meta-delete"), + + /** + * Patch operation + */ + PATCH("patch"), ; 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 f0a1a32d277..3bfb5bee98c 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 @@ -1,7 +1,5 @@ package ca.uhn.fhir.rest.client; -import static org.apache.commons.lang3.StringUtils.isNotBlank; - /* * #%L * HAPI FHIR - Core Library @@ -22,33 +20,20 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * #L% */ -import java.io.ByteArrayInputStream; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.StringReader; -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.Set; +import java.util.*; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; -import org.hl7.fhir.instance.model.api.IBase; -import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; -import org.hl7.fhir.instance.model.api.IBaseResource; -import org.hl7.fhir.instance.model.api.IIdType; -import org.hl7.fhir.instance.model.api.IPrimitiveType; +import org.hl7.fhir.instance.model.api.*; -import ca.uhn.fhir.context.BaseRuntimeChildDefinition; -import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; -import ca.uhn.fhir.context.BaseRuntimeElementDefinition; -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.context.*; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.SummaryEnum; @@ -67,6 +52,7 @@ 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.util.OperationOutcomeUtil; +import ca.uhn.fhir.util.XmlUtil; public abstract class BaseClient implements IRestfulClient { @@ -105,6 +91,11 @@ public abstract class BaseClient implements IRestfulClient { if ("true".equals(System.getProperty(HAPI_CLIENT_KEEPRESPONSES))) { setKeepResponses(true); } + + if (XmlUtil.isStaxPresent() == false) { + myEncoding = EncodingEnum.JSON; + } + } protected Map> createExtraParams() { 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 895e795a5f1..9b39d0df5e7 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 @@ -69,6 +69,7 @@ import ca.uhn.fhir.model.primitive.UriDt; import ca.uhn.fhir.parser.DataFormatException; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.PatchTypeEnum; import ca.uhn.fhir.rest.api.PreferReturnEnum; import ca.uhn.fhir.rest.api.SortOrderEnum; import ca.uhn.fhir.rest.api.SortSpec; @@ -107,6 +108,11 @@ import ca.uhn.fhir.rest.gclient.IOperationUntyped; import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInput; import ca.uhn.fhir.rest.gclient.IOperationUntypedWithInputAndPartialOutput; import ca.uhn.fhir.rest.gclient.IParam; +import ca.uhn.fhir.rest.gclient.IPatch; +import ca.uhn.fhir.rest.gclient.IPatchExecutable; +import ca.uhn.fhir.rest.gclient.IPatchTyped; +import ca.uhn.fhir.rest.gclient.IPatchWithQuery; +import ca.uhn.fhir.rest.gclient.IPatchWithQueryTyped; import ca.uhn.fhir.rest.gclient.IQuery; import ca.uhn.fhir.rest.gclient.IRead; import ca.uhn.fhir.rest.gclient.IReadExecutable; @@ -519,6 +525,32 @@ public class GenericClient extends BaseClient implements IGenericClient { return new ArrayList(resp.toListOfResources()); } + + + @Override + public IPatch patch() { + return new PatchInternal(); + } + + @Override + public MethodOutcome patch(IdDt theIdDt, IBaseResource theResource) { + BaseHttpClientInvocation invocation = MethodUtil.createUpdateInvocation(theResource, null, theIdDt, myContext); + if (isKeepResponses()) { + myLastRequest = invocation.asHttpRequest(getServerBase(), createExtraParams(), getEncoding(), isPrettyPrint()); + } + + RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource); + final String resourceName = def.getName(); + + OutcomeResponseHandler binding = new OutcomeResponseHandler(resourceName); + MethodOutcome resp = invokeClient(myContext, binding, invocation, myLogRequestAndResponse); + return resp; + } + + @Override + public MethodOutcome patch(String theId, IBaseResource theResource) { + return update(new IdDt(theId), theResource); + } @Override public IUpdate update() { @@ -2287,6 +2319,139 @@ public class GenericClient extends BaseClient implements IGenericClient { } } + + private class PatchInternal extends BaseClientExecutable implements IPatch, IPatchTyped, IPatchExecutable, IPatchWithQuery, IPatchWithQueryTyped { + + private CriterionList myCriterionList; + private IIdType myId; + private PreferReturnEnum myPrefer; + private IBaseResource myResource; + private String myResourceBody; + private String mySearchUrl; + private PatchTypeEnum myPatchType; + private String myPatchBody; + + @Override + public IPatchWithQueryTyped and(ICriterion theCriterion) { + myCriterionList.add((ICriterionInternal) theCriterion); + return this; + } + + @Override + public IPatchWithQuery conditional() { + myCriterionList = new CriterionList(); + return this; + } + + @Override + public IPatchTyped conditionalByUrl(String theSearchUrl) { + mySearchUrl = validateAndEscapeConditionalUrl(theSearchUrl); + return this; + } + + @Override + public MethodOutcome execute() { + if (myResource == null) { + myResource = parseResourceBody(myResourceBody); + } + + // If an explicit encoding is chosen, we will re-serialize to ensure the right encoding + if (getParamEncoding() != null) { + myResourceBody = null; + } + + if (myPatchType == null) { + throw new InvalidRequestException("No patch type supplied, cannot invoke server"); + } + if (myPatchBody == null) { + throw new InvalidRequestException("No patch body supplied, cannot invoke server"); + } + + + if (myId == null) { + myId = myResource.getIdElement(); + } + + if (myId == null || myId.hasIdPart() == false) { + throw new InvalidRequestException("No ID supplied for resource to update, can not invoke server"); + } + BaseHttpClientInvocation invocation = MethodUtil.createPatchInvocation(myContext, myId, myPatchType, myPatchBody); + + addPreferHeader(myPrefer, invocation); + + RuntimeResourceDefinition def = myContext.getResourceDefinition(myResource); + final String resourceName = def.getName(); + + OutcomeResponseHandler binding = new OutcomeResponseHandler(resourceName, myPrefer); + + Map> params = new HashMap>(); + return invoke(params, binding, invocation); + + } + + @Override + public IPatchExecutable prefer(PreferReturnEnum theReturn) { + myPrefer = theReturn; + return this; + } + + @Override + public IPatchTyped resource(IBaseResource theResource) { + Validate.notNull(theResource, "Resource can not be null"); + myResource = theResource; + return this; + } + + @Override + public IPatchTyped resource(String theResourceBody) { + Validate.notBlank(theResourceBody, "Body can not be null or blank"); + myResourceBody = theResourceBody; + return this; + } + + @Override + public IPatchWithQueryTyped where(ICriterion theCriterion) { + myCriterionList.add((ICriterionInternal) theCriterion); + return this; + } + + @Override + public IPatchExecutable withId(IIdType theId) { + if (theId == null) { + throw new NullPointerException("theId can not be null"); + } + if (theId.hasIdPart() == false) { + throw new NullPointerException("theId must not be blank and must contain an ID, found: " + theId.getValue()); + } + myId = theId; + return this; + } + + @Override + public IPatchExecutable withId(String theId) { + if (theId == null) { + throw new NullPointerException("theId can not be null"); + } + if (isBlank(theId)) { + throw new NullPointerException("theId must not be blank and must contain an ID, found: " + theId); + } + myId = new IdDt(theId); + return this; + } + + @Override + public IPatchTyped patchType(PatchTypeEnum patchType) { + myPatchType = patchType; + return this; + } + + @Override + public IPatchTyped patchBody(String patchBody) { + myPatchBody = patchBody; + return this; + } + + } private class UpdateInternal extends BaseClientExecutable implements IUpdate, IUpdateTyped, IUpdateExecutable, IUpdateWithQuery, IUpdateWithQueryTyped { 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 ac49faf4b8d..7d79af36e3e 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 @@ -46,6 +46,7 @@ import ca.uhn.fhir.rest.gclient.IGetTags; import ca.uhn.fhir.rest.gclient.IHistory; import ca.uhn.fhir.rest.gclient.IMeta; import ca.uhn.fhir.rest.gclient.IOperation; +import ca.uhn.fhir.rest.gclient.IPatch; import ca.uhn.fhir.rest.gclient.IRead; import ca.uhn.fhir.rest.gclient.ITransaction; import ca.uhn.fhir.rest.gclient.IUntypedQuery; @@ -253,6 +254,35 @@ public interface IGenericClient extends IRestfulClient { @Override void registerInterceptor(IClientInterceptor theInterceptor); + + /** + * Fluent method for the "patch" operation, which performs a logical patch on a server resource + */ + IPatch patch(); + + /** + * Implementation of the "instance patch" method. + * + * @param theId + * The ID to update + * @param theResource + * The new resource body + * @return An outcome containing the results and possibly the new version ID + */ + MethodOutcome patch(IdDt theId, IBaseResource theResource); + + /** + * Implementation of the "instance update" method. + * + * @param theId + * The ID to update + * @param theResource + * The new resource body + * @return An outcome containing the results and possibly the new version ID + */ + MethodOutcome patch(String theId, IBaseResource theResource); + + /** * Search for resources matching a given set of criteria. Searching is a very powerful * feature in FHIR with many features for specifying exactly what should be seaerched for diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttpClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttpClient.java index 48ac411ea50..f635e236bb6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttpClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttpClient.java @@ -30,12 +30,7 @@ import org.apache.http.HttpEntity; import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpOptions; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.client.methods.*; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.message.BasicNameValuePair; import org.hl7.fhir.instance.model.api.IBaseBinary; @@ -188,6 +183,10 @@ public class ApacheHttpClient implements IHttpClient { switch (myRequestType) { case DELETE: return new HttpDelete(url); + case PATCH: + HttpPatch httpPatch = new HttpPatch(url); + httpPatch.setEntity(theEntity); + return httpPatch; case OPTIONS: return new HttpOptions(url); case POST: diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatch.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatch.java new file mode 100644 index 00000000000..7fc26ed0d7f --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatch.java @@ -0,0 +1,31 @@ +package ca.uhn.fhir.rest.gclient; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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 org.hl7.fhir.instance.model.api.IBaseResource; + +public interface IPatch { + + IPatchTyped resource(IBaseResource theResource); + + IPatchTyped resource(String theResourceBody); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchExecutable.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchExecutable.java new file mode 100644 index 00000000000..3fb718598f3 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchExecutable.java @@ -0,0 +1,37 @@ +package ca.uhn.fhir.rest.gclient; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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.api.MethodOutcome; +import ca.uhn.fhir.rest.api.PreferReturnEnum; + +public interface IPatchExecutable extends IClientExecutable{ + + /** + * Add a Prefer header to the request, which requests that the server include + * or suppress the resource body as a part of the result. If a resource is returned by the server + * it will be parsed an accessible to the client via {@link MethodOutcome#getResource()} + * + * @since HAPI 1.1 + */ + IPatchExecutable prefer(PreferReturnEnum theReturn); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchTyped.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchTyped.java new file mode 100644 index 00000000000..e38522587ac --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchTyped.java @@ -0,0 +1,61 @@ +package ca.uhn.fhir.rest.gclient; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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 org.hl7.fhir.instance.model.api.IIdType; + +import ca.uhn.fhir.rest.api.PatchTypeEnum; + +public interface IPatchTyped extends IPatchExecutable { + + IPatchExecutable withId(IIdType theId); + + IPatchExecutable withId(String theId); + + /** + * Specifies that the update should be performed as a conditional create + * against a given search URL. + * + * @param theSearchUrl The search URL to use. The format of this URL should be of the form [ResourceType]?[Parameters], + * for example: Patient?name=Smith&identifier=13.2.4.11.4%7C847366 + * @since HAPI 0.9 / FHIR DSTU 2 + */ + IPatchTyped conditionalByUrl(String theSearchUrl); + + /** + * @since HAPI 0.9 / FHIR DSTU 2 + */ + IPatchWithQuery conditional(); + + /** + * Specifies the format of the patch (either XML or JSON) + * @param patchType + */ + IPatchTyped patchType(PatchTypeEnum patchType); + + /** + * The body of the patch document serialized in either XML or JSON which conforms to + * http://jsonpatch.com/ or http://tools.ietf.org/html/rfc5261 + * @param patchBody + */ + IPatchTyped patchBody(String patchBody); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchWithQuery.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchWithQuery.java new file mode 100644 index 00000000000..32b94c5c02c --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchWithQuery.java @@ -0,0 +1,26 @@ +package ca.uhn.fhir.rest.gclient; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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 IPatchWithQuery extends IBaseQuery { + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchWithQueryTyped.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchWithQueryTyped.java new file mode 100644 index 00000000000..cdc639f1dfa --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IPatchWithQueryTyped.java @@ -0,0 +1,25 @@ +package ca.uhn.fhir.rest.gclient; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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 IPatchWithQueryTyped extends IPatchTyped, IPatchWithQuery { + +} 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 cb485611393..65b387cd5e6 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 @@ -211,8 +211,12 @@ abstract class BaseHttpClientInvocationWithContents extends BaseHttpClientInvoca private String getContentType(EncodingEnum encoding) { if (myBundle != null || (getContext().getVersion().getVersion() == FhirVersionEnum.DSTU1 && ((myContents != null && myContentsIsBundle) || myResources != null))) { return encoding.getBundleContentType(); - } else { + } else if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { + // application/xml+fhir return encoding.getResourceContentType(); + } else { + // application/fhir+xml + return encoding.getResourceContentTypeNonLegacy(); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java index b4155cede8d..5e540dd1584 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseMethodBinding.java @@ -46,20 +46,7 @@ import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.TagList; import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.rest.annotation.AddTags; -import ca.uhn.fhir.rest.annotation.Create; -import ca.uhn.fhir.rest.annotation.Delete; -import ca.uhn.fhir.rest.annotation.DeleteTags; -import ca.uhn.fhir.rest.annotation.GetPage; -import ca.uhn.fhir.rest.annotation.GetTags; -import ca.uhn.fhir.rest.annotation.History; -import ca.uhn.fhir.rest.annotation.Metadata; -import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.Read; -import ca.uhn.fhir.rest.annotation.Search; -import ca.uhn.fhir.rest.annotation.Transaction; -import ca.uhn.fhir.rest.annotation.Update; -import ca.uhn.fhir.rest.annotation.Validate; +import ca.uhn.fhir.rest.annotation.*; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; @@ -387,9 +374,10 @@ public abstract class BaseMethodBinding implements IClientResponseHandler Transaction transaction = theMethod.getAnnotation(Transaction.class); Operation operation = theMethod.getAnnotation(Operation.class); GetPage getPage = theMethod.getAnnotation(GetPage.class); + Patch patch = theMethod.getAnnotation(Patch.class); // ** if you add another annotation above, also add it to the next line: - if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, getTags, addTags, deleteTags, transaction, operation, getPage)) { + if (!verifyMethodHasZeroOrOneOperationAnnotation(theMethod, read, search, conformance, create, update, delete, history, validate, getTags, addTags, deleteTags, transaction, operation, getPage, patch)) { return null; } @@ -414,7 +402,7 @@ public abstract class BaseMethodBinding implements IClientResponseHandler throw new ConfigurationException("Method '" + theMethod.getName() + "' from type " + theMethod.getDeclaringClass().getCanonicalName() + " is annotated with @" + GetTags.class.getSimpleName() + " but does not return type " + TagList.class.getName()); } - } else if (MethodOutcome.class.equals(returnTypeFromMethod)) { + } else if (MethodOutcome.class.isAssignableFrom(returnTypeFromMethod)) { // returns a method outcome } else if (IBundleProvider.class.equals(returnTypeFromMethod)) { // returns a bundle provider @@ -448,6 +436,8 @@ public abstract class BaseMethodBinding implements IClientResponseHandler returnTypeFromAnnotation = history.type(); } else if (delete != null) { returnTypeFromAnnotation = delete.type(); + } else if (patch != null) { + returnTypeFromAnnotation = patch.type(); } else if (create != null) { returnTypeFromAnnotation = create.type(); } else if (update != null) { @@ -513,6 +503,8 @@ public abstract class BaseMethodBinding implements IClientResponseHandler return new UpdateMethodBinding(theMethod, theContext, theProvider); } else if (delete != null) { return new DeleteMethodBinding(theMethod, theContext, theProvider); + } else if (patch != null) { + return new PatchMethodBinding(theMethod, theContext, theProvider); } else if (history != null) { return new HistoryMethodBinding(theMethod, theContext, theProvider); } else if (validate != null) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java index bcd7c825b7e..e39173ef7fe 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBinding.java @@ -184,8 +184,15 @@ abstract class BaseOutcomeReturningMethodBinding extends BaseMethodBinding theMethodAnnotationType, Class theResourceTypeFromAnnotation) { + super(theMethod, theContext, theMethodAnnotationType, theProvider); + + Class resourceType = theResourceTypeFromAnnotation; + if (resourceType != IBaseResource.class) { + RuntimeResourceDefinition def = theContext.getResourceDefinition(resourceType); + myResourceName = def.getName(); + } else { + if (theProvider != null && theProvider instanceof IResourceProvider) { + RuntimeResourceDefinition def = theContext.getResourceDefinition(((IResourceProvider) theProvider).getResourceType()); + myResourceName = def.getName(); + } else { + throw new ConfigurationException( + "Can not determine resource type for method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getCanonicalName() + " - Did you forget to include the resourceType() value on the @" + Delete.class.getSimpleName() + " method annotation?"); + } + } + + myIdParameterIndex = MethodUtil.findIdParameterIndex(theMethod, getContext()); + if (myIdParameterIndex == null) { + throw new ConfigurationException("Method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() + "' has no parameter annotated with the @" + IdParam.class.getSimpleName() + " annotation"); + } + + Integer versionIdParameterIndex = MethodUtil.findVersionIdParameterIndex(theMethod); + if (versionIdParameterIndex != null) { + throw new ConfigurationException("Method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() + "' has a parameter annotated with the @" + VersionIdParam.class.getSimpleName() + " annotation but delete methods may not have this annotation"); + } + + } + + @Override + public String getResourceName() { + return myResourceName; + } + + protected Integer getIdParameterIndex() { + return myIdParameterIndex; + } + + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DeleteMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DeleteMethodBinding.java index 1642794a39a..f24326cde6d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DeleteMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/DeleteMethodBinding.java @@ -10,7 +10,7 @@ package ca.uhn.fhir.rest.method; * 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 + * 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, @@ -28,59 +28,19 @@ import java.util.Set; import org.hl7.fhir.instance.model.api.IIdType; -import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.RuntimeResourceDefinition; import ca.uhn.fhir.model.api.IResource; -import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.annotation.Delete; -import ca.uhn.fhir.rest.annotation.IdParam; -import ca.uhn.fhir.rest.annotation.VersionIdParam; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; -import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -public class DeleteMethodBinding extends BaseOutcomeReturningMethodBinding { - - private String myResourceName; - private Integer myIdParameterIndex; +public class DeleteMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceIdButNoResourceBody { public DeleteMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { - super(theMethod, theContext, Delete.class, theProvider); - - Delete deleteAnnotation = theMethod.getAnnotation(Delete.class); - Class resourceType = deleteAnnotation.type(); - if (resourceType != IResource.class) { - RuntimeResourceDefinition def = theContext.getResourceDefinition(resourceType); - myResourceName = def.getName(); - } else { - if (theProvider != null && theProvider instanceof IResourceProvider) { - RuntimeResourceDefinition def = theContext.getResourceDefinition(((IResourceProvider) theProvider).getResourceType()); - myResourceName = def.getName(); - } else { - throw new ConfigurationException( - "Can not determine resource type for method '" + theMethod.getName() + "' on type " + theMethod.getDeclaringClass().getCanonicalName() + " - Did you forget to include the resourceType() value on the @" + Delete.class.getSimpleName() + " method annotation?"); - } - } - - myIdParameterIndex = MethodUtil.findIdParameterIndex(theMethod, getContext()); - if (myIdParameterIndex == null) { - throw new ConfigurationException("Method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() + "' has no parameter annotated with the @" + IdParam.class.getSimpleName() + " annotation"); - } - - Integer versionIdParameterIndex = MethodUtil.findVersionIdParameterIndex(theMethod); - if (versionIdParameterIndex != null) { - throw new ConfigurationException("Method '" + theMethod.getName() + "' on type '" + theMethod.getDeclaringClass().getCanonicalName() + "' has a parameter annotated with the @" + VersionIdParam.class.getSimpleName() + " annotation but delete methods may not have this annotation"); - } - - } - - @Override - protected boolean allowVoidReturnType() { - return true; + super(theMethod, theContext, theProvider, Delete.class, theMethod.getAnnotation(Delete.class).type()); } @Override @@ -102,13 +62,13 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBinding { } @Override - public String getResourceName() { - return myResourceName; + protected boolean allowVoidReturnType() { + return true; } @Override public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { - IdDt idDt = (IdDt) theArgs[myIdParameterIndex]; + IIdType idDt = (IIdType) theArgs[getIdParameterIndex()]; if (idDt == null) { throw new NullPointerException("ID can not be null"); } @@ -136,7 +96,7 @@ public class DeleteMethodBinding extends BaseOutcomeReturningMethodBinding { @Override protected void addParametersForServerRequest(RequestDetails theRequest, Object[] theParams) { - theParams[myIdParameterIndex] = theRequest.getId(); + theParams[getIdParameterIndex()] = theRequest.getId(); } @Override diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpDeleteClientInvocation.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpDeleteClientInvocation.java index e7e869d8976..7cce411bada 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpDeleteClientInvocation.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpDeleteClientInvocation.java @@ -67,4 +67,10 @@ public class HttpDeleteClientInvocation extends BaseHttpClientInvocation { return createHttpRequest(b.toString(), theEncoding, RequestTypeEnum.DELETE); } + @Override + protected IHttpRequest createHttpRequest(String theUrl, EncodingEnum theEncoding, RequestTypeEnum theRequestType) { + // TODO Auto-generated method stub + return super.createHttpRequest(theUrl, theEncoding, theRequestType); + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpPatchClientInvocation.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpPatchClientInvocation.java new file mode 100644 index 00000000000..2969fb4d063 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/HttpPatchClientInvocation.java @@ -0,0 +1,84 @@ +package ca.uhn.fhir.rest.method; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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.util.List; +import java.util.Map; + +import org.hl7.fhir.instance.model.api.IIdType; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; +import ca.uhn.fhir.rest.client.api.IHttpClient; +import ca.uhn.fhir.rest.client.api.IHttpRequest; +import ca.uhn.fhir.rest.server.EncodingEnum; + +public class HttpPatchClientInvocation extends BaseHttpClientInvocation { + + private String myUrlPath; + private Map> myParams; + private String myContents; + private String myContentType; + + public HttpPatchClientInvocation(FhirContext theContext, IIdType theId, String theContentType, String theContents) { + super(theContext); + myUrlPath = theId.toUnqualifiedVersionless().getValue(); + myContentType = theContentType; + myContents = theContents; + } +// +// public HttpDeleteClientInvocation(FhirContext theContext, String theSearchUrl) { +// super(theContext); +// myUrlPath = theSearchUrl; +// } +// +// public HttpDeleteClientInvocation(FhirContext theContext, String theResourceType, Map> theParams) { +// super(theContext); +// myUrlPath = theResourceType; +// myParams = theParams; +// } + + @Override + public IHttpRequest asHttpRequest(String theUrlBase, Map> theExtraParams, EncodingEnum theEncoding, Boolean thePrettyPrint) { + StringBuilder b = new StringBuilder(); + b.append(theUrlBase); + if (!theUrlBase.endsWith("/")) { + b.append('/'); + } + b.append(myUrlPath); + + appendExtraParamsWithQuestionMark(myParams, b, b.indexOf("?") == -1); + appendExtraParamsWithQuestionMark(theExtraParams, b, b.indexOf("?") == -1); + + + + return createHttpRequest(b.toString(), theEncoding, RequestTypeEnum.PATCH); + } + + @Override + protected IHttpRequest createHttpRequest(String theUrl, EncodingEnum theEncoding, RequestTypeEnum theRequestType) { + IHttpClient httpClient = getRestfulClientFactory().getHttpClient(new StringBuilder(theUrl), null, null, theRequestType, getHeaders()); + return httpClient.createByteRequest(getContext(), myContents, myContentType, null); + } + + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java index c5f8f2ed557..d52243fdd35 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/MethodUtil.java @@ -22,10 +22,7 @@ import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.annotation.*; -import ca.uhn.fhir.rest.api.MethodOutcome; -import ca.uhn.fhir.rest.api.RestOperationTypeEnum; -import ca.uhn.fhir.rest.api.SummaryEnum; -import ca.uhn.fhir.rest.api.ValidationModeEnum; +import ca.uhn.fhir.rest.api.*; import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; import ca.uhn.fhir.rest.method.OperationParameter.IOperationParamConverter; import ca.uhn.fhir.rest.param.*; @@ -158,6 +155,12 @@ public class MethodUtil { retVal.setIfNoneExistString(theIfNoneExistUrl); return retVal; } + + public static HttpPatchClientInvocation createPatchInvocation(FhirContext theContext, IIdType theId, PatchTypeEnum thePatchType, String theBody) { + return PatchMethodBinding.createPatchInvocation(theContext, theId, thePatchType, theBody); + } + + /** End Patch **/ public static HttpPutClientInvocation createUpdateInvocation(FhirContext theContext, IBaseResource theResource, String theResourceBody, Map> theMatchParams) { StringBuilder b = new StringBuilder(); @@ -188,6 +191,7 @@ public class MethodUtil { return retVal; } + public static HttpPutClientInvocation createUpdateInvocation(FhirContext theContext, IBaseResource theResource, String theResourceBody, String theMatchUrl) { HttpPutClientInvocation retVal; if (StringUtils.isBlank(theResourceBody)) { @@ -364,6 +368,8 @@ public class MethodUtil { param = new RequestOperationCallbackParameter(); } else if (parameterType.equals(SummaryEnum.class)) { param = new SummaryEnumParameter(); + } else if (parameterType.equals(PatchTypeEnum.class)) { + param = new PatchTypeParameter(); } else { for (int i = 0; i < annotations.length && param == null; i++) { Annotation nextAnnotation = annotations[i]; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/PatchMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/PatchMethodBinding.java new file mode 100644 index 00000000000..37d9514c2ae --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/PatchMethodBinding.java @@ -0,0 +1,153 @@ +package ca.uhn.fhir.rest.method; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.ListIterator; +import java.util.Set; + +import org.hl7.fhir.instance.model.api.IIdType; + +import ca.uhn.fhir.context.ConfigurationException; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.model.api.IResource; +import ca.uhn.fhir.rest.annotation.Patch; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.api.PatchTypeEnum; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.client.BaseHttpClientInvocation; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; + +/** + * Base class for an operation that has a resource type but not a resource body in the + * request body + * + */ +public class PatchMethodBinding extends BaseOutcomeReturningMethodBindingWithResourceIdButNoResourceBody { + + private int myPatchTypeParameterIndex = -1; + private int myResourceParamIndex; + + public PatchMethodBinding(Method theMethod, FhirContext theContext, Object theProvider) { + super(theMethod, theContext, theProvider, Patch.class, theMethod.getAnnotation(Patch.class).type()); + + for (ListIterator> iter = Arrays.asList(theMethod.getParameterTypes()).listIterator(); iter.hasNext();) { + int nextIndex = iter.nextIndex(); + Class next = iter.next(); + if (next.equals(PatchTypeEnum.class)) { + myPatchTypeParameterIndex = nextIndex; + } + for (Annotation nextAnnotation : theMethod.getParameterAnnotations()[nextIndex]) { + if (nextAnnotation instanceof ResourceParam) { + myResourceParamIndex = nextIndex; + } + } + } + + if (myPatchTypeParameterIndex == -1) { + throw new ConfigurationException("Method has no parameter of type " + PatchTypeEnum.class.getName() + " - " + theMethod.toString()); + } + if (myResourceParamIndex == -1) { + throw new ConfigurationException("Method has no parameter with @" + ResourceParam.class.getSimpleName() + " annotation - " + theMethod.toString()); + } + } + + @Override + public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) { + boolean retVal = super.incomingServerRequestMatchesMethod(theRequest); + if (retVal) { + PatchTypeParameter.getTypeForRequestOrThrowInvalidRequestException(theRequest); + } + return retVal; + } + + @Override + public RestOperationTypeEnum getRestOperationType() { + return RestOperationTypeEnum.PATCH; + } + + @Override + protected Set provideAllowableRequestTypes() { + return Collections.singleton(RequestTypeEnum.PATCH); + } + + @Override + protected BaseHttpClientInvocation createClientInvocation(Object[] theArgs, IResource theResource) { + StringBuilder urlExtension = new StringBuilder(); + urlExtension.append(getContext().getResourceDefinition(theResource).getName()); + + return new HttpPostClientInvocation(getContext(), theResource, urlExtension.toString()); + } + + @Override + protected boolean allowVoidReturnType() { + return true; + } + + @Override + public BaseHttpClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException { + IIdType idDt = (IIdType) theArgs[getIdParameterIndex()]; + if (idDt == null) { + throw new NullPointerException("ID can not be null"); + } + + if (idDt.hasResourceType() == false) { + idDt = idDt.withResourceType(getResourceName()); + } else if (getResourceName().equals(idDt.getResourceType()) == false) { + throw new InvalidRequestException("ID parameter has the wrong resource type, expected '" + getResourceName() + "', found: " + idDt.getResourceType()); + } + + PatchTypeEnum patchType = (PatchTypeEnum) theArgs[myPatchTypeParameterIndex]; + String body = (String) theArgs[myResourceParamIndex]; + + HttpPatchClientInvocation retVal = createPatchInvocation(getContext(), idDt, patchType, body); + + for (int idx = 0; idx < theArgs.length; idx++) { + IParameter nextParam = getParameters().get(idx); + nextParam.translateClientArgumentIntoQueryArgument(getContext(), theArgs[idx], null, null); + } + + return retVal; + } + + public static HttpPatchClientInvocation createPatchInvocation(FhirContext theContext, IIdType theId, PatchTypeEnum thePatchType, String theBody) { + HttpPatchClientInvocation retVal = new HttpPatchClientInvocation(theContext, theId, thePatchType.getContentType(), theBody); + return retVal; + } + + @Override + protected void addParametersForServerRequest(RequestDetails theRequest, Object[] theParams) { + IIdType id = theRequest.getId(); + id = UpdateMethodBinding.applyETagAsVersion(theRequest, id); + theParams[getIdParameterIndex()] = id; + } + + @Override + protected String getMatchingOperation() { + return null; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/PatchTypeParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/PatchTypeParameter.java new file mode 100644 index 00000000000..32a46c94d54 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/PatchTypeParameter.java @@ -0,0 +1,72 @@ +package ca.uhn.fhir.rest.method; + +import static org.apache.commons.lang3.StringUtils.defaultString; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 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.lang.reflect.Method; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.PatchTypeEnum; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; + +class PatchTypeParameter implements IParameter { + @Override + public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException { + // nothing + } + + @Override + public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { + return getTypeForRequestOrThrowInvalidRequestException(theRequest); + } + + @Override + public void initializeTypes(Method theMethod, Class> theOuterCollectionType, Class> theInnerCollectionType, Class theParameterType) { + // ignore + } + + public static PatchTypeEnum getTypeForRequestOrThrowInvalidRequestException(RequestDetails theRequest) { + String contentTypeAll = defaultString(theRequest.getHeader(Constants.HEADER_CONTENT_TYPE)); + String contentType = contentTypeAll; + int semiColonIdx = contentType.indexOf(';'); + if (semiColonIdx != -1) { + contentType = contentTypeAll.substring(0, semiColonIdx); + } + contentType = contentType.trim(); + if (Constants.CT_JSON_PATCH.equals(contentType)) { + return PatchTypeEnum.JSON_PATCH; + } else if (Constants.CT_XML_PATCH.equals(contentType)) { + return PatchTypeEnum.XML_PATCH; + } else { + throw new InvalidRequestException("Invalid Content-Type for PATCH operation: " + contentTypeAll); + } + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServletRequestParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServletRequestParameter.java index 77fd774a3e9..e4624dce4b3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServletRequestParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ServletRequestParameter.java @@ -35,6 +35,10 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; class ServletRequestParameter implements IParameter { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServletRequestParameter.class); + ServletRequestParameter() { + super(); + } + @Override public void translateClientArgumentIntoQueryArgument(FhirContext theContext, Object theSourceClientArgument, Map> theTargetQueryArguments, IBaseResource theTargetResource) throws InternalErrorException { /* diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java index d30f57b4554..8140346752e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java @@ -69,13 +69,7 @@ public class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe } } - String ifMatchValue = theRequest.getHeader(Constants.HEADER_IF_MATCH); - if (isNotBlank(ifMatchValue)) { - ifMatchValue = MethodUtil.parseETagValue(ifMatchValue); - if (id != null && id.hasVersionIdPart() == false) { - id = id.withVersion(ifMatchValue); - } - } + id = applyETagAsVersion(theRequest, id); if (theRequest.getId() != null && theRequest.getId().hasVersionIdPart() == false) { if (id != null && id.hasVersionIdPart()) { @@ -94,6 +88,17 @@ public class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe super.addParametersForServerRequest(theRequest, theParams); } + public static IIdType applyETagAsVersion(RequestDetails theRequest, IIdType id) { + String ifMatchValue = theRequest.getHeader(Constants.HEADER_IF_MATCH); + if (isNotBlank(ifMatchValue)) { + ifMatchValue = MethodUtil.parseETagValue(ifMatchValue); + if (id != null && id.hasVersionIdPart() == false) { + id = id.withVersion(ifMatchValue); + } + } + return id; + } + @Override protected BaseHttpClientInvocation createClientInvocation(Object[] theArgs, IResource theResource) { IdDt idDt = (IdDt) theArgs[myIdParameterIndex]; 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 22a17a003ab..a7c87d1bb23 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 @@ -63,6 +63,8 @@ public class Constants { public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding"; public static final String HEADER_ACCEPT_VALUE_XML_OR_JSON_LEGACY = CT_FHIR_XML + ";q=1.0, " + CT_FHIR_JSON + ";q=1.0"; public static final String HEADER_ACCEPT_VALUE_XML_OR_JSON_NON_LEGACY = CT_FHIR_XML_NEW + ";q=1.0, " + CT_FHIR_JSON_NEW + ";q=1.0, " + HEADER_ACCEPT_VALUE_XML_OR_JSON_LEGACY.replace("1.0", "0.9"); + public static final String HEADER_ACCEPT_VALUE_XML_NON_LEGACY = CT_FHIR_XML_NEW + ";q=1.0, " + CT_FHIR_XML + ";q=0.9"; + public static final String HEADER_ACCEPT_VALUE_JSON_NON_LEGACY = CT_FHIR_JSON_NEW + ";q=1.0, " + CT_FHIR_JSON + ";q=0.9"; public static final String HEADER_ALLOW = "Allow"; public static final String HEADER_AUTHORIZATION = "Authorization"; public static final String HEADER_AUTHORIZATION_VALPREFIX_BASIC = "Basic "; @@ -166,6 +168,8 @@ public class Constants { public static final String TAG_SUBSETTED_SYSTEM = "http://hl7.org/fhir/v3/ObservationValue"; public static final String URL_TOKEN_HISTORY = "_history"; public static final String URL_TOKEN_METADATA = "metadata"; + public static final String CT_JSON_PATCH = "application/json-patch+json"; + public static final String CT_XML_PATCH = "application/xml-patch+xml"; static { CHARSET_UTF8 = Charset.forName(CHARSET_NAME_UTF8); 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 b62bc691d42..59b7d6e5b84 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 @@ -29,15 +29,8 @@ import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.StringTokenizer; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.jar.Manifest; @@ -78,14 +71,16 @@ import ca.uhn.fhir.rest.server.interceptor.ExceptionHandlingInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import ca.uhn.fhir.util.CoverageIgnore; -import ca.uhn.fhir.util.ReflectionUtil; -import ca.uhn.fhir.util.UrlPathTokenizer; -import ca.uhn.fhir.util.UrlUtil; -import ca.uhn.fhir.util.VersionUtil; +import ca.uhn.fhir.util.*; public class RestfulServer extends HttpServlet implements IRestfulServer { + /** + * All incoming requests will have an attribute added to {@link HttpServletRequest#getAttribute(String)} + * with this key. The value will be a Java {@link Date} with the time that request processing began. + */ + public static final String REQUEST_START_TIME = RestfulServer.class.getName() + "REQUEST_START_TIME"; + /** * Default setting for {@link #setETagSupport(ETagSupportEnum) ETag Support}: {@link ETagSupportEnum#ENABLED} */ @@ -658,6 +653,11 @@ public class RestfulServer extends HttpServlet implements IRestfulServer= 0; i--) { + IServerInterceptor next = getInterceptors().get(i); + next.processingCompletedNormally(requestDetails); + } + } catch (NotModifiedException e) { for (int i = getInterceptors().size() - 1; i >= 0; i--) { @@ -1163,6 +1163,8 @@ public class RestfulServer extends HttpServlet implements IRestfulServer * This method may however throw a subclass of {@link BaseServerResponseException}, and processing @@ -292,7 +295,8 @@ public interface IServerInterceptor { * This exception may be thrown to indicate that the interceptor has detected an unauthorized access * attempt. If thrown, processing will stop and an HTTP 401 will be returned to the client. */ - boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException; + boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) + throws AuthenticationException; /** * This method is called after the server implementation method has been called, but before any attempt to stream the @@ -364,6 +368,18 @@ public interface IServerInterceptor { */ BaseServerResponseException preProcessOutgoingException(RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theServletRequest) throws ServletException; + /** + * This method is called after all processing is completed for a request, but only if the + * request completes normally (i.e. no exception is thrown). + *

+ * Note that this individual interceptors will have this method called in the reverse order from the order in + * which the interceptors were registered with the server. + *

+ * @param theRequestDetails + * The request itself + */ + void processingCompletedNormally(ServletRequestDetails theRequestDetails); + public static class ActionRequestDetails { private final FhirContext myContext; private final IIdType myId; @@ -400,20 +416,22 @@ public interface IServerInterceptor { myResource = theResource; } - public ActionRequestDetails(RequestDetails theRequestDetails, String theResourceType, IIdType theId) { - this(theRequestDetails, theRequestDetails.getServer().getFhirContext(), theResourceType, theId); - } - /** * Constructor * - * @param theRequestDetails The request details to wrap - * @param theId The ID of the resource being created (note that the ID should have the resource type populated) + * @param theRequestDetails + * The request details to wrap + * @param theId + * The ID of the resource being created (note that the ID should have the resource type populated) */ public ActionRequestDetails(RequestDetails theRequestDetails, IIdType theId) { this(theRequestDetails, theId.getResourceType(), theId); } + public ActionRequestDetails(RequestDetails theRequestDetails, String theResourceType, IIdType theId) { + this(theRequestDetails, theRequestDetails.getServer().getFhirContext(), theResourceType, theId); + } + public FhirContext getContext() { return myContext; } @@ -454,14 +472,14 @@ public interface IServerInterceptor { } /** - * Returns the same map which was + * Returns the same map which was */ public Map getUserData() { return myRequestDetails.getUserData(); } /** - * This method may be invoked by user code to notify interceptors that a nested + * This method may be invoked by user code to notify interceptors that a nested * operation is being invoked which is denoted by this request details. */ public void notifyIncomingRequestPreHandled(RestOperationTypeEnum theOperationType) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.java index d6a5affbd04..3c383629881 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/InterceptorAdapter.java @@ -10,7 +10,7 @@ package ca.uhn.fhir.rest.server.interceptor; * 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 + * 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, @@ -38,13 +38,13 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; /** * Base class for {@link IServerInterceptor} implementations. Provides a No-op implementation - * of all methods, always returning true + * of all methods, always returning true */ public class InterceptorAdapter implements IServerInterceptor { @Override - public boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws ServletException, - IOException { + public boolean handleException(RequestDetails theRequestDetails, BaseServerResponseException theException, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) + throws ServletException, IOException { return true; } @@ -64,52 +64,60 @@ public class InterceptorAdapter implements IServerInterceptor { } @Override - public boolean outgoingResponse(RequestDetails theRequestDetails, Bundle theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { + public boolean outgoingResponse(RequestDetails theRequestDetails) { + ServletRequestDetails details = (ServletRequestDetails) theRequestDetails; + return outgoingResponse(theRequestDetails, details.getServletRequest(), details.getServletResponse()); + } + + @Override + public boolean outgoingResponse(RequestDetails theRequestDetails, Bundle bundle) { + ServletRequestDetails details = (ServletRequestDetails) theRequestDetails; + return outgoingResponse(details, bundle, details.getServletRequest(), details.getServletResponse()); + } + + @Override + public boolean outgoingResponse(RequestDetails theRequestDetails, Bundle theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) + throws AuthenticationException { return true; } - - @Override - public boolean outgoingResponse(RequestDetails theRequestDetails, Bundle bundle) { - ServletRequestDetails details = (ServletRequestDetails) theRequestDetails; - return outgoingResponse(details, bundle, details.getServletRequest(), details.getServletResponse()); - } @Override public boolean outgoingResponse(RequestDetails theRequestDetails, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { return true; } - - @Override - public boolean outgoingResponse(RequestDetails theRequestDetails) { - ServletRequestDetails details = (ServletRequestDetails) theRequestDetails; - return outgoingResponse(theRequestDetails, details.getServletRequest(), details.getServletResponse()); - } @Override - public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { - return true; + public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) { + ServletRequestDetails details = (ServletRequestDetails) theRequestDetails; + return outgoingResponse(details, theResponseObject, details.getServletRequest(), details.getServletResponse()); } - - @Override - public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) { - ServletRequestDetails details = (ServletRequestDetails) theRequestDetails; - return outgoingResponse(details, theResponseObject, details.getServletRequest(), details.getServletResponse()); - } - + @Override - public boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) throws AuthenticationException { + public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) + throws AuthenticationException { + return true; + } + + @Override + public boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject) { + ServletRequestDetails details = (ServletRequestDetails) theRequestDetails; + return outgoingResponse(details, theResponseObject, details.getServletRequest(), details.getServletResponse()); + } + + @Override + public boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse) + throws AuthenticationException { return true; } - - @Override - public boolean outgoingResponse(RequestDetails theRequestDetails, TagList theResponseObject) { - ServletRequestDetails details = (ServletRequestDetails) theRequestDetails; - return outgoingResponse(details, theResponseObject, details.getServletRequest(), details.getServletResponse()); - } @Override public BaseServerResponseException preProcessOutgoingException(RequestDetails theRequestDetails, Throwable theException, HttpServletRequest theServletRequest) throws ServletException { return null; } + @Override + public void processingCompletedNormally(ServletRequestDetails theRequestDetails) { + // nothing + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptor.java index 01c75e4935b..8babecec29f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptor.java @@ -26,6 +26,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.util.Date; import java.util.Map.Entry; import javax.servlet.ServletException; @@ -41,11 +42,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServerUtils; import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; /** * Server interceptor which logs each request using a defined format @@ -74,7 +78,7 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; * * * ${remoteAddr} - * The originaring IP of the request + * The originating IP of the request * * * ${requestHeader.XXXX} @@ -115,12 +119,21 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; * * */ + +/* + * TODO: implement this, but it needs the logging to happen at the end + * + * ${processingTimeMillis} + * The number of milliseconds spent processing this request + * + + */ public class LoggingInterceptor extends InterceptorAdapter { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(LoggingInterceptor.class); - private String myErrorMessageFormat = "ERROR - ${idOrResourceName}"; - private boolean myLogExceptions; + private String myErrorMessageFormat = "ERROR - ${operationType} - ${idOrResourceName}"; + private boolean myLogExceptions = true; private Logger myLogger = ourLog; private String myMessageFormat = "${operationType} - ${idOrResourceName}"; @@ -146,18 +159,16 @@ public class LoggingInterceptor extends InterceptorAdapter { return true; } - @Override - public boolean incomingRequestPostProcessed(final RequestDetails theRequestDetails, final HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException { + @Override + public void processingCompletedNormally(ServletRequestDetails theRequestDetails) { // Perform any string substitutions from the message format - StrLookup lookup = new MyLookup(theRequest, theRequestDetails); + StrLookup lookup = new MyLookup(theRequestDetails.getServletRequest(), theRequestDetails); StrSubstitutor subs = new StrSubstitutor(lookup, "${", "}", '\\'); // Actuall log the line String line = subs.replace(myMessageFormat); myLogger.info(line); - - return true; } /** @@ -308,10 +319,16 @@ public class LoggingInterceptor extends InterceptorAdapter { EncodingEnum encoding = EncodingEnum.forContentType(contentType); if (encoding != null) { byte[] requestContents = myRequestDetails.loadRequestContents(); - return new String(requestContents, Charsets.UTF_8); + return new String(requestContents, Constants.CHARSET_UTF8); } } return ""; + } else if ("processingTimeMillis".equals(theKey)) { + Date startTime = (Date) myRequest.getAttribute(RestfulServer.REQUEST_START_TIME); + if (startTime != null) { + long time = System.currentTimeMillis() - startTime.getTime(); + return Long.toString(time); + } } return "!VAL!"; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java index bfd47b2fd42..9addc8c9269 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlighterInterceptor.java @@ -24,10 +24,12 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; */ import java.io.IOException; +import java.util.Date; import java.util.Map; import java.util.Set; import javax.servlet.ServletException; +import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -39,6 +41,7 @@ import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.RestfulServerUtils; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; @@ -206,7 +209,7 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter { return super.handleException(theRequestDetails, theException, theServletRequest, theServletResponse); } - streamResponse(theRequestDetails, theServletResponse, theException.getOperationOutcome()); + streamResponse(theRequestDetails, theServletResponse, theException.getOperationOutcome(), theServletRequest); return false; } @@ -276,12 +279,12 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter { return super.outgoingResponse(theRequestDetails, theResponseObject, theServletRequest, theServletResponse); } - streamResponse(theRequestDetails, theServletResponse, theResponseObject); + streamResponse(theRequestDetails, theServletResponse, theResponseObject, theServletRequest); return false; } - private void streamResponse(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, IBaseResource resource) { + private void streamResponse(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, IBaseResource resource, ServletRequest theServletRequest) { IParser p; Map parameters = theRequestDetails.getParameters(); if (parameters.containsKey(Constants.PARAM_FORMAT)) { @@ -360,6 +363,16 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter { b.append("HTML XML."); + + Date startTime = (Date) theServletRequest.getAttribute(RestfulServer.REQUEST_START_TIME); + if (startTime != null) { + long time = System.currentTimeMillis() - startTime.getTime(); + b.append(" Response generated in "); + b.append(time); + b.append("ms."); + } + + b.append("

"); b.append("\n"); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/XmlUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/XmlUtil.java index 98f62579210..55d6cf74e70 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/XmlUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/XmlUtil.java @@ -19,26 +19,12 @@ package ca.uhn.fhir.util; * limitations under the License. * #L% */ - -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.io.Writer; +import java.io.*; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import javax.xml.stream.FactoryConfigurationError; -import javax.xml.stream.XMLEventReader; -import javax.xml.stream.XMLEventWriter; -import javax.xml.stream.XMLInputFactory; -import javax.xml.stream.XMLOutputFactory; -import javax.xml.stream.XMLResolver; -import javax.xml.stream.XMLStreamException; -import javax.xml.stream.XMLStreamWriter; +import javax.xml.stream.*; import org.apache.commons.lang3.StringEscapeUtils; import org.codehaus.stax2.XMLOutputFactory2; @@ -47,6 +33,7 @@ import org.codehaus.stax2.io.EscapingWriterFactory; import com.ctc.wstx.api.WstxInputProperties; import com.ctc.wstx.stax.WstxOutputFactory; +import ca.uhn.fhir.context.ConfigurationException; import ca.uhn.fhir.util.jar.DependencyLogFactory; import ca.uhn.fhir.util.jar.IDependencyLog; @@ -62,6 +49,7 @@ public class XmlUtil { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(XmlUtil.class); private static Throwable ourNextException; private static volatile XMLOutputFactory ourOutputFactory; + private static Boolean ourStaxPresent; private static final Map VALID_ENTITY_NAMES; private static final ExtendedEntityReplacingXmlResolver XML_RESOLVER = new ExtendedEntityReplacingXmlResolver(); @@ -1528,8 +1516,8 @@ public class XmlUtil { // ok } - XMLOutputFactory outputFactory = XMLOutputFactory.newInstance(); - + XMLOutputFactory outputFactory = newOutputFactory(); + if (!ourHaveLoggedStaxImplementation) { logStaxImplementation(outputFactory.getClass()); } @@ -1601,8 +1589,7 @@ public class XmlUtil { // ok } - XMLInputFactory inputFactory; - inputFactory = XMLInputFactory.newInstance(); + XMLInputFactory inputFactory = newInputFactory(); if (!ourHaveLoggedStaxImplementation) { logStaxImplementation(inputFactory.getClass()); @@ -1645,7 +1632,6 @@ public class XmlUtil { return ourInputFactory; } - private static XMLOutputFactory getOrCreateOutputFactory() throws FactoryConfigurationError { if (ourOutputFactory == null) { ourOutputFactory = createOutputFactory(); @@ -1661,6 +1647,29 @@ public class XmlUtil { ourHaveLoggedStaxImplementation = true; } + + static XMLInputFactory newInputFactory() throws FactoryConfigurationError { + XMLInputFactory inputFactory; + try { + inputFactory = XMLInputFactory.newInstance(); + throwUnitTestExceptionIfConfiguredToDoSo(); + } catch (Throwable e) { + throw new ConfigurationException("Unable to initialize StAX - XML processing is disabled", e); + } + return inputFactory; + } + + static XMLOutputFactory newOutputFactory() throws FactoryConfigurationError { + XMLOutputFactory outputFactory; + try { + outputFactory = XMLOutputFactory.newInstance(); + throwUnitTestExceptionIfConfiguredToDoSo(); + } catch (Throwable e) { + throw new ConfigurationException("Unable to initialize StAX - XML processing is disabled", e); + } + return outputFactory; + } + /** * FOR UNIT TESTS ONLY - Throw this exception for the next operation */ @@ -1691,6 +1700,26 @@ public class XmlUtil { } } + /** + * This method will return true if a StAX XML parsing library is present + * on the classpath + */ + public static boolean isStaxPresent() { + Boolean retVal = ourStaxPresent; + if (retVal == null) { + try { + newInputFactory(); + ourStaxPresent = Boolean.TRUE; + retVal = Boolean.TRUE; + } catch (ConfigurationException e) { + ourLog.info("StAX not detected on classpath, XML processing will be disabled"); + ourStaxPresent = Boolean.FALSE; + retVal = Boolean.FALSE; + } + } + return retVal; + } + public static class MyEscaper implements EscapingWriterFactory { @Override diff --git a/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/App.java b/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/App.java index edd27fee83b..4f882dfd986 100644 --- a/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/App.java +++ b/hapi-fhir-cli/hapi-fhir-cli-app/src/main/java/ca/uhn/fhir/cli/App.java @@ -180,7 +180,9 @@ public class App { try { String[] args = Arrays.asList(theArgs).subList(1, theArgs.length).toArray(new String[theArgs.length - 1]); parsedOptions = parser.parse(options, args, true); - + if (parsedOptions.getArgList().isEmpty()==false) { + throw new ParseException("Unrecognized argument: " + parsedOptions.getArgList().get(0).toString()); + } // Actually execute the command command.run(parsedOptions); diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml index 508e0733b33..f84e913f113 100644 --- a/hapi-fhir-jacoco/pom.xml +++ b/hapi-fhir-jacoco/pom.xml @@ -232,6 +232,7 @@ hapi-fhir-structures-dstu3/target/jacoco.exec hapi-fhir-jpaserver-base/target/jacoco.exec hapi-fhir-client-okhttp/target/jacoco.exec + hapi-fhir-android/target/jacoco.exec diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProvider.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProvider.java index 12e9af1504b..6d4cee89274 100644 --- a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProvider.java +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProvider.java @@ -57,7 +57,7 @@ import ca.uhn.fhir.rest.server.IRestfulServer; * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare */ @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN }) -@Consumes({ MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON, Constants.CT_FHIR_JSON, Constants.CT_FHIR_XML }) +@Consumes({ MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON, Constants.CT_FHIR_JSON, Constants.CT_FHIR_XML, Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_XML_NEW }) @Interceptors(JaxRsExceptionInterceptor.class) public abstract class AbstractJaxRsResourceProvider extends AbstractJaxRsProvider diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/client/GenericJaxRsClientDstu3Test.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/client/GenericJaxRsClientDstu3Test.java index 2605b3c65ef..623bdbb86d3 100644 --- a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/client/GenericJaxRsClientDstu3Test.java +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/client/GenericJaxRsClientDstu3Test.java @@ -267,7 +267,7 @@ public class GenericJaxRsClientDstu3Test { client.create().resource(p).execute(); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertThat(ourRequestBodyString, containsString("")); assertEquals("http://localhost:" + ourPort + "/fhir/Patient", ourRequestUri); assertEquals("POST", ourRequestMethod); @@ -277,7 +277,7 @@ public class GenericJaxRsClientDstu3Test { client.create().resource(p).execute(); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); String body = ourRequestBodyString; assertThat(body, containsString("")); assertThat(body, not(containsString("123"))); @@ -302,7 +302,7 @@ public class GenericJaxRsClientDstu3Test { client.create().resource(p).conditionalByUrl("Patient?name=foo").execute(); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertThat(ourRequestBodyString, containsString("")); assertEquals("http://localhost:" + ourPort + "/fhir/Patient", ourRequestUri); assertEquals("http://localhost:" + ourPort + "/fhir/Patient?name=foo", ourRequestFirstHeaders.get(Constants.HEADER_IF_NONE_EXIST).getValue()); @@ -311,7 +311,7 @@ public class GenericJaxRsClientDstu3Test { client.create().resource(p).conditionalByUrl("Patient?name=http://foo|bar").execute(); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertThat(ourRequestBodyString, containsString("")); assertEquals("http://localhost:" + ourPort + "/fhir/Patient", ourRequestUri); assertEquals("http://localhost:" + ourPort + "/fhir/Patient?name=http%3A//foo%7Cbar", ourRequestFirstHeaders.get(Constants.HEADER_IF_NONE_EXIST).getValue()); @@ -320,7 +320,7 @@ public class GenericJaxRsClientDstu3Test { client.create().resource(p).conditional().where(Patient.NAME.matches().value("foo")).execute(); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertThat(ourRequestBodyString, containsString("")); assertEquals("http://localhost:" + ourPort + "/fhir/Patient", ourRequestUri); assertEquals("http://localhost:" + ourPort + "/fhir/Patient?name=foo", ourRequestFirstHeaders.get(Constants.HEADER_IF_NONE_EXIST).getValue()); @@ -344,7 +344,7 @@ public class GenericJaxRsClientDstu3Test { client.create(p); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertThat(ourRequestBodyString, containsString("")); assertEquals("http://localhost:" + ourPort + "/fhir/Patient", ourRequestUri); assertEquals("POST", ourRequestMethod); @@ -839,7 +839,7 @@ public class GenericJaxRsClientDstu3Test { //@formatter:on assertEquals("http://localhost:" + ourPort + "/fhir/$SOMEOPERATION", ourRequestUri); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertEquals(ourRequestBodyString, reqString); assertEquals("POST", ourRequestMethod); assertEquals(1, resp.getParameter().size()); @@ -875,7 +875,7 @@ public class GenericJaxRsClientDstu3Test { assertEquals("http://localhost:" + ourPort + "/fhir/$SOMEOPERATION", ourRequestUri); assertEquals(respString, p.encodeResourceToString(resp)); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertEquals("POST", ourRequestMethod); assertEquals("", (ourRequestBodyString)); @@ -896,7 +896,7 @@ public class GenericJaxRsClientDstu3Test { assertEquals("http://localhost:" + ourPort + "/fhir/$SOMEOPERATION", ourRequestUri); assertEquals(respString, p.encodeResourceToString(resp)); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertEquals("POST", ourRequestMethod); assertEquals("", (ourRequestBodyString)); @@ -918,7 +918,7 @@ public class GenericJaxRsClientDstu3Test { assertEquals("http://localhost:" + ourPort + "/fhir/$SOMEOPERATION", ourRequestUri); assertEquals(respString, p.encodeResourceToString(resp)); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertEquals("POST", ourRequestMethod); assertEquals( "", @@ -1049,7 +1049,7 @@ public class GenericJaxRsClientDstu3Test { assertEquals("http://localhost:" + ourPort + "/fhir/$SOMEOPERATION", ourRequestUri); assertEquals(respString, p.encodeResourceToString(resp)); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertEquals(ourRequestBodyString, reqString); assertEquals("POST", ourRequestMethod); @@ -1064,7 +1064,7 @@ public class GenericJaxRsClientDstu3Test { assertEquals("http://localhost:" + ourPort + "/fhir/Patient/$SOMEOPERATION", ourRequestUri); assertEquals(respString, p.encodeResourceToString(resp)); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertEquals(ourRequestBodyString, reqString); assertEquals("POST", ourRequestMethod); @@ -1079,7 +1079,7 @@ public class GenericJaxRsClientDstu3Test { assertEquals("http://localhost:" + ourPort + "/fhir/Patient/123/$SOMEOPERATION", ourRequestUri); assertEquals(respString, p.encodeResourceToString(resp)); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertEquals(ourRequestBodyString, reqString); assertEquals("POST", ourRequestMethod); @@ -1122,7 +1122,7 @@ public class GenericJaxRsClientDstu3Test { assertEquals("http://localhost:" + ourPort + "/fhir/$SOMEOPERATION", ourRequestUri); assertEquals(respString, p.encodeResourceToString(resp)); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertEquals(ourRequestBodyString, reqString); assertEquals("POST", ourRequestMethod); @@ -1137,7 +1137,7 @@ public class GenericJaxRsClientDstu3Test { assertEquals("http://localhost:" + ourPort + "/fhir/Patient/$SOMEOPERATION", ourRequestUri); assertEquals(respString, p.encodeResourceToString(resp)); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertEquals(ourRequestBodyString, reqString); assertEquals("POST", ourRequestMethod); @@ -1152,7 +1152,7 @@ public class GenericJaxRsClientDstu3Test { assertEquals("http://localhost:" + ourPort + "/fhir/Patient/123/$SOMEOPERATION", ourRequestUri); assertEquals(respString, p.encodeResourceToString(resp)); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertEquals(ourRequestBodyString, reqString); assertEquals("POST", ourRequestMethod); @@ -1644,7 +1644,7 @@ public class GenericJaxRsClientDstu3Test { assertEquals("name=james", ourRequestBodyString); assertEquals("application/x-www-form-urlencoded", ourRequestContentType); - assertEquals(Constants.CT_FHIR_JSON, ourRequestFirstHeaders.get("Accept").getValue()); + assertEquals(Constants.HEADER_ACCEPT_VALUE_JSON_NON_LEGACY, ourRequestFirstHeaders.get("Accept").getValue()); } @Test @@ -1843,7 +1843,7 @@ public class GenericJaxRsClientDstu3Test { assertEquals("http://localhost:" + ourPort + "/fhir/", ourRequestUri); assertThat(response, containsString("\"Bundle\"")); - assertEquals("application/json+fhir;charset=UTF-8", ourRequestFirstHeaders.get("Content-Type").getValue()); + assertEquals("application/fhir+json;charset=UTF-8", ourRequestFirstHeaders.get("Content-Type").getValue()); //@formatter:off response = client.transaction() @@ -1853,7 +1853,7 @@ public class GenericJaxRsClientDstu3Test { //@formatter:on assertEquals("http://localhost:" + ourPort + "/fhir/", ourRequestUri); - assertEquals("application/xml+fhir;charset=UTF-8", ourRequestFirstHeaders.get("Content-Type").getValue()); + assertEquals("application/fhir+xml;charset=UTF-8", ourRequestFirstHeaders.get("Content-Type").getValue()); } @@ -1913,7 +1913,7 @@ public class GenericJaxRsClientDstu3Test { client.update().resource(p).conditionalByUrl("Patient?name=foo").execute(); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertThat(ourRequestBodyString, containsString("")); assertEquals("PUT", ourRequestMethod); assertEquals("http://localhost:" + ourPort + "/fhir/Patient?name=foo", ourRequestUri); @@ -1921,7 +1921,7 @@ public class GenericJaxRsClientDstu3Test { client.update().resource(p).conditionalByUrl("Patient?name=http://foo|bar").execute(); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertThat(ourRequestBodyString, containsString("")); assertEquals("PUT", ourRequestMethod); assertEquals("http://localhost:" + ourPort + "/fhir/Patient?name=http%3A//foo%7Cbar", ourRequestUri); @@ -1929,7 +1929,7 @@ public class GenericJaxRsClientDstu3Test { client.update().resource(ourCtx.newXmlParser().encodeResourceToString(p)).conditionalByUrl("Patient?name=foo").execute(); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertThat(ourRequestBodyString, containsString("")); assertEquals("PUT", ourRequestMethod); assertEquals("http://localhost:" + ourPort + "/fhir/Patient?name=foo", ourRequestUri); @@ -1937,7 +1937,7 @@ public class GenericJaxRsClientDstu3Test { client.update().resource(p).conditional().where(Patient.NAME.matches().value("foo")).and(Patient.ADDRESS.matches().value("AAA|BBB")).execute(); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertThat(ourRequestBodyString, containsString("")); assertEquals("PUT", ourRequestMethod); assertEquals("http://localhost:" + ourPort + "/fhir/Patient?name=foo&address=AAA%5C%7CBBB", ourRequestUri); @@ -1945,7 +1945,7 @@ public class GenericJaxRsClientDstu3Test { client.update().resource(ourCtx.newXmlParser().encodeResourceToString(p)).conditional().where(Patient.NAME.matches().value("foo")).and(Patient.ADDRESS.matches().value("AAA|BBB")).execute(); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertThat(ourRequestBodyString, containsString("")); assertEquals("PUT", ourRequestMethod); assertEquals("http://localhost:" + ourPort + "/fhir/Patient?name=foo&address=AAA%5C%7CBBB", ourRequestUri); @@ -1968,7 +1968,7 @@ public class GenericJaxRsClientDstu3Test { client.update(new IdType("Patient/123").getValue(), p); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertThat(ourRequestBodyString, containsString("")); assertEquals("http://localhost:" + ourPort + "/fhir/Patient/123", ourRequestUri); assertEquals("PUT", ourRequestMethod); @@ -1976,7 +1976,7 @@ public class GenericJaxRsClientDstu3Test { client.update("123", p); assertEquals(1, ourRequestHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); - assertEquals(EncodingEnum.XML.getResourceContentType() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); + assertEquals(EncodingEnum.XML.getResourceContentTypeNonLegacy() + Constants.HEADER_SUFFIX_CT_UTF_8, ourRequestFirstHeaders.get(Constants.HEADER_CONTENT_TYPE).getValue().replace(";char", "; char")); assertThat(ourRequestBodyString, containsString("")); assertEquals("http://localhost:" + ourPort + "/fhir/Patient/123", ourRequestUri); assertEquals("PUT", ourRequestMethod); diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsMockPatientRestProviderDstu3.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsMockPatientRestProviderDstu3.java index 2e0e1e07031..c36a5a1d6bf 100644 --- a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsMockPatientRestProviderDstu3.java +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/test/TestJaxRsMockPatientRestProviderDstu3.java @@ -48,7 +48,7 @@ import ca.uhn.fhir.rest.server.IPagingProvider; */ @Path(TestJaxRsMockPatientRestProviderDstu3.PATH) @Stateless -@Produces({ MediaType.APPLICATION_JSON, Constants.CT_FHIR_JSON, Constants.CT_FHIR_XML }) +@Produces({ MediaType.APPLICATION_JSON, Constants.CT_FHIR_JSON, Constants.CT_FHIR_XML, Constants.CT_FHIR_JSON_NEW, Constants.CT_FHIR_XML_NEW }) @Interceptors(JaxRsExceptionInterceptor.class) public class TestJaxRsMockPatientRestProviderDstu3 extends AbstractJaxRsResourceProvider { diff --git a/hapi-fhir-jpaserver-base/.gitignore b/hapi-fhir-jpaserver-base/.gitignore index ec1f25fea96..bc3ff7634b3 100644 --- a/hapi-fhir-jpaserver-base/.gitignore +++ b/hapi-fhir-jpaserver-base/.gitignore @@ -1,4 +1,5 @@ ca.uhn.fhir.jpa.entity.ResourceTable/ +ca.*/ target/ /bin nohup.out diff --git a/hapi-fhir-jpaserver-base/ca.uhn.fhir.jpa.entity.TermConcept/segments_1 b/hapi-fhir-jpaserver-base/ca.uhn.fhir.jpa.entity.TermConcept/segments_1 deleted file mode 100644 index c6a50652363..00000000000 Binary files a/hapi-fhir-jpaserver-base/ca.uhn.fhir.jpa.entity.TermConcept/segments_1 and /dev/null differ diff --git a/hapi-fhir-jpaserver-base/pom.xml b/hapi-fhir-jpaserver-base/pom.xml index f4dafd1a4e7..c9352d8fdcc 100644 --- a/hapi-fhir-jpaserver-base/pom.xml +++ b/hapi-fhir-jpaserver-base/pom.xml @@ -99,7 +99,18 @@ org.jscience jscience
- + + + + net.riotopsys + json_patch + + + com.github.dnault + xml-patch + 0.3.0 + + + + ca.uhn.hapi.fhir + hapi-fhir + 2.1-SNAPSHOT + ../../pom.xml + + + ca.uhn.hapi.fhir.example + hapi-fhir-jpaserver-example-postgres + war + + HAPI FHIR JPA Server - Example (Postgres) + + + + + + ca.uhn.hapi.fhir + hapi-fhir-base + 2.1-SNAPSHOT + + + + + ca.uhn.hapi.fhir + hapi-fhir-jpaserver-base + 2.1-SNAPSHOT + + + + + ca.uhn.hapi.fhir + hapi-fhir-testpage-overlay + 2.1-SNAPSHOT + war + provided + + + ca.uhn.hapi.fhir + hapi-fhir-testpage-overlay + 2.1-SNAPSHOT + classes + provided + + + + + ch.qos.logback + logback-classic + + + + + javax.servlet + javax.servlet-api + provided + + + + + org.thymeleaf + thymeleaf + + + + + org.ebaysf.web + cors-filter + + + servlet-api + javax.servlet + + + + + + + org.springframework + spring-web + + + + + org.apache.commons + commons-dbcp2 + + + + + org.apache.derby + derby + + + org.apache.derby + derbynet + + + org.apache.derby + derbyclient + + + + + org.eclipse.jetty + jetty-servlets + test + + + org.eclipse.jetty + jetty-servlet + test + + + org.eclipse.jetty + jetty-server + test + + + org.eclipse.jetty + jetty-util + test + + + org.eclipse.jetty + jetty-webapp + test + + + com.phloc + phloc-schematron + + + + org.postgresql + postgresql + 9.4.1210.jre7 + + + + + + + + hapi-fhir-jpaserver-example + + + + + + org.eclipse.jetty + jetty-maven-plugin + + + /hapi-fhir-jpaserver-example + true + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + + + + + + org.apache.maven.plugins + maven-war-plugin + + + + ${maven.build.timestamp} + + + + + ca.uhn.hapi.fhir + hapi-fhir-testpage-overlay + + + src/main/webapp/WEB-INF/web.xml + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + true + + + + + integration-test + verify + + + + + + + + + a + diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/readme.intellij.txt b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/readme.intellij.txt new file mode 100644 index 00000000000..049d5d341a5 --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/readme.intellij.txt @@ -0,0 +1,31 @@ +Running hapi-fhir-jpaserver-example in Tomcat from IntelliJ + +Install Tomcat. + +Make sure you have Tomcat set up in IntelliJ. +File->Settings->Build, Execution, Deployment->Application Servers +Click + +Select "Tomcat Server" +Enter the path to your tomcat deployment for both Tomcat Home (IntelliJ will fill in base directory for you) + +Add a Run Configuration for running hapi-fhir-jpaserver-example under Tomcat +Run->Edit Configurations +Click the green + +Select Tomcat Server, Local +Change the name to whatever you wish +Uncheck the "After launch" checkbox +On the "Deployment" tab, click the green + +Select "Artifact" +Select "hapi-fhir-jpaserver-example:war" +In "Application context" type /hapi + +Run the configuration +You should now have an "Application Servers" in the list of windows at the bottom. +Click it. +Select your server, and click the green triangle (or the bug if you want to debug) +Wait for the console output to stop + +Point your browser (or fiddler, or what have you) to +http://localhost:8080/hapi/base/Patient + +You should get an empty bundle back. \ No newline at end of file diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java new file mode 100644 index 00000000000..4a19ef39012 --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/FhirServerConfig.java @@ -0,0 +1,129 @@ +package ca.uhn.fhir.jpa.demo; + +import java.util.Properties; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; + +import org.apache.commons.dbcp2.BasicDataSource; +import org.apache.commons.lang3.time.DateUtils; +import org.hibernate.jpa.HibernatePersistenceProvider; +import org.springframework.beans.factory.annotation.Autowire; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu3; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; +import ca.uhn.fhir.rest.server.interceptor.LoggingInterceptor; +import ca.uhn.fhir.rest.server.interceptor.ResponseHighlighterInterceptor; + +/** + * This class isn't used by default by the example, but + * you can use it as a config if you want to support DSTU3 + * instead of DSTU2 in your server. + * + * See https://github.com/jamesagnew/hapi-fhir/issues/278 + */ +@Configuration +@EnableTransactionManagement() +public class FhirServerConfig extends BaseJavaConfigDstu3 { + + /** + * Configure FHIR properties around the the JPA server via this bean + */ + @Bean() + public DaoConfig daoConfig() { + DaoConfig retVal = new DaoConfig(); + retVal.setSubscriptionEnabled(true); + retVal.setSubscriptionPollDelay(5000); + retVal.setSubscriptionPurgeInactiveAfterMillis(DateUtils.MILLIS_PER_HOUR); + retVal.setAllowMultipleDelete(true); + return retVal; + } + + /** + * The following bean configures the database connection. The 'url' property value of "jdbc:derby:directory:jpaserver_derby_files;create=true" indicates that the server should save resources in a + * directory called "jpaserver_derby_files". + * + * A URL to a remote database could also be placed here, along with login credentials and other properties supported by BasicDataSource. + */ + @Bean(destroyMethod = "close") + public DataSource dataSource() { + BasicDataSource retVal = new BasicDataSource(); + retVal.setDriver(new org.postgresql.Driver()); + retVal.setUrl("jdbc:postgresql://localhost:5432/hapi"); + retVal.setUsername("hapi"); + retVal.setPassword("mysecretpassword"); + return retVal; + } + + @Bean() + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + LocalContainerEntityManagerFactoryBean retVal = new LocalContainerEntityManagerFactoryBean(); + retVal.setPersistenceUnitName("HAPI_PU"); + retVal.setDataSource(dataSource()); + retVal.setPackagesToScan("ca.uhn.fhir.jpa.entity"); + retVal.setPersistenceProvider(new HibernatePersistenceProvider()); + retVal.setJpaProperties(jpaProperties()); + return retVal; + } + + private Properties jpaProperties() { + Properties extraProperties = new Properties(); + extraProperties.put("hibernate.dialect", org.hibernate.dialect.PostgreSQL94Dialect.class.getName()); + extraProperties.put("hibernate.format_sql", "true"); + extraProperties.put("hibernate.show_sql", "false"); + extraProperties.put("hibernate.hbm2ddl.auto", "update"); + extraProperties.put("hibernate.jdbc.batch_size", "20"); + extraProperties.put("hibernate.cache.use_query_cache", "false"); + extraProperties.put("hibernate.cache.use_second_level_cache", "false"); + extraProperties.put("hibernate.cache.use_structured_entries", "false"); + extraProperties.put("hibernate.cache.use_minimal_puts", "false"); + extraProperties.put("hibernate.search.default.directory_provider", "filesystem"); + extraProperties.put("hibernate.search.default.indexBase", "target/lucenefiles"); + extraProperties.put("hibernate.search.lucene_version", "LUCENE_CURRENT"); +// extraProperties.put("hibernate.search.default.worker.execution", "async"); + return extraProperties; + } + + /** + * Do some fancy logging to create a nice access log that has details about each incoming request. + */ + public IServerInterceptor loggingInterceptor() { + LoggingInterceptor retVal = new LoggingInterceptor(); + retVal.setLoggerName("fhirtest.access"); + retVal.setMessageFormat( + "Path[${servletPath}] Source[${requestHeader.x-forwarded-for}] Operation[${operationType} ${operationName} ${idOrResourceName}] UA[${requestHeader.user-agent}] Params[${requestParameters}] ResponseEncoding[${responseEncodingNoDefault}]"); + retVal.setLogExceptions(true); + retVal.setErrorMessageFormat("ERROR - ${requestVerb} ${requestUrl}"); + return retVal; + } + + /** + * This interceptor adds some pretty syntax highlighting in responses when a browser is detected + */ + @Bean(autowire = Autowire.BY_TYPE) + public IServerInterceptor responseHighlighterInterceptor() { + ResponseHighlighterInterceptor retVal = new ResponseHighlighterInterceptor(); + return retVal; + } + + @Bean(autowire = Autowire.BY_TYPE) + public IServerInterceptor subscriptionSecurityInterceptor() { + SubscriptionsRequireManualActivationInterceptorDstu3 retVal = new SubscriptionsRequireManualActivationInterceptorDstu3(); + return retVal; + } + + @Bean() + public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { + JpaTransactionManager retVal = new JpaTransactionManager(); + retVal.setEntityManagerFactory(entityManagerFactory); + return retVal; + } + +} diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfig.java b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfig.java new file mode 100644 index 00000000000..22c9e4d1067 --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/FhirTesterConfig.java @@ -0,0 +1,51 @@ +package ca.uhn.fhir.jpa.demo; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import ca.uhn.fhir.context.FhirVersionEnum; +import ca.uhn.fhir.to.FhirTesterMvcConfig; +import ca.uhn.fhir.to.TesterConfig; + +//@formatter:off +/** + * This spring config file configures the web testing module. It serves two + * purposes: + * 1. It imports FhirTesterMvcConfig, which is the spring config for the + * tester itself + * 2. It tells the tester which server(s) to talk to, via the testerConfig() + * method below + */ +@Configuration +@Import(FhirTesterMvcConfig.class) +public class FhirTesterConfig { + + /** + * This bean tells the testing webpage which servers it should configure itself + * to communicate with. In this example we configure it to talk to the local + * server, as well as one public server. If you are creating a project to + * deploy somewhere else, you might choose to only put your own server's + * address here. + * + * Note the use of the ${serverBase} variable below. This will be replaced with + * the base URL as reported by the server itself. Often for a simple Tomcat + * (or other container) installation, this will end up being something + * like "http://localhost:8080/hapi-fhir-jpaserver-example". If you are + * deploying your server to a place with a fully qualified domain name, + * you might want to use that instead of using the variable. + */ + @Bean + public TesterConfig testerConfig() { + TesterConfig retVal = new TesterConfig(); + retVal + .addServer() + .withId("home") + .withFhirVersion(FhirVersionEnum.DSTU3) + .withBaseUrl("${serverBase}/baseDstu3") + .withName("Local Tester"); + return retVal; + } + +} +//@formatter:on diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java new file mode 100644 index 00000000000..8b51ff4bc54 --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java @@ -0,0 +1,124 @@ +package ca.uhn.fhir.jpa.demo; + +import java.util.Collection; +import java.util.List; + +import javax.servlet.ServletException; + +import org.hl7.fhir.dstu3.model.Meta; +import org.springframework.web.context.ContextLoaderListener; +import org.springframework.web.context.WebApplicationContext; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.IFhirSystemDao; +import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3; +import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; +import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3; +import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; +import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; +import ca.uhn.fhir.rest.server.ETagSupportEnum; +import ca.uhn.fhir.rest.server.EncodingEnum; +import ca.uhn.fhir.rest.server.IResourceProvider; +import ca.uhn.fhir.rest.server.RestfulServer; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; + +public class JpaServerDemo extends RestfulServer { + + private static final long serialVersionUID = 1L; + + private WebApplicationContext myAppCtx; + + @SuppressWarnings("unchecked") + @Override + protected void initialize() throws ServletException { + super.initialize(); + + /* + * We want to support FHIR DSTU3 format. This means that the server + * will use the DSTU3 bundle format and other DSTU3 encoding changes. + * + * If you want to use DSTU1 instead, change the following line, and change the 3 occurrences of dstu2 in web.xml to dstu1 + */ + setFhirContext(FhirContext.forDstu3()); + + // Get the spring context from the web container (it's declared in web.xml) + myAppCtx = ContextLoaderListener.getCurrentWebApplicationContext(); + + /* + * The BaseJavaConfigDstu3.java class is a spring configuration + * file which is automatically generated as a part of hapi-fhir-jpaserver-base and + * contains bean definitions for a resource provider for each resource type + */ + List beans = myAppCtx.getBean("myResourceProvidersDstu3", List.class); + setResourceProviders(beans); + + /* + * The system provider implements non-resource-type methods, such as + * transaction, and global history. + */ + setPlainProviders(myAppCtx.getBean("mySystemProviderDstu3", JpaSystemProviderDstu3.class)); + + /* + * The conformance provider exports the supported resources, search parameters, etc for + * this server. The JPA version adds resource counts to the exported statement, so it + * is a nice addition. + */ + IFhirSystemDao systemDao = myAppCtx.getBean("mySystemDaoDstu3", IFhirSystemDao.class); + JpaConformanceProviderDstu3 confProvider = new JpaConformanceProviderDstu3(this, systemDao, myAppCtx.getBean(DaoConfig.class)); + confProvider.setImplementationDescription("Example Server"); + setServerConformanceProvider(confProvider); + + /* + * Enable ETag Support (this is already the default) + */ + setETagSupport(ETagSupportEnum.ENABLED); + + /* + * This server tries to dynamically generate narratives + */ + FhirContext ctx = getFhirContext(); + ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator()); + + /* + * Default to JSON and pretty printing + */ + setDefaultPrettyPrint(true); + setDefaultResponseEncoding(EncodingEnum.JSON); + + /* + * -- New in HAPI FHIR 1.5 -- + * This configures the server to page search results to and from + * the database, instead of only paging them to memory. This may mean + * a performance hit when performing searches that return lots of results, + * but makes the server much more scalable. + */ + setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class)); + + /* + * Load interceptors for the server from Spring (these are defined in FhirServerConfig.java) + */ + Collection interceptorBeans = myAppCtx.getBeansOfType(IServerInterceptor.class).values(); + for (IServerInterceptor interceptor : interceptorBeans) { + this.registerInterceptor(interceptor); + } + + /* + * If you are hosting this server at a specific DNS name, the server will try to + * figure out the FHIR base URL based on what the web container tells it, but + * this doesn't always work. If you are setting links in your search bundles that + * just refer to "localhost", you might want to use a server address strategy: + */ + //setServerAddressStrategy(new HardcodedServerAddressStrategy("http://mydomain.com/fhir/baseDstu2")); + + /* + * If you are using DSTU3+, you may want to add a terminology uploader, which allows + * uploading of external terminologies such as Snomed CT. Note that this uploader + * does not have any security attached (any anonymous user may use it by default) + * so it is a potential security vulnerability. Consider using an AuthorizationInterceptor + * with this feature. + */ + registerProvider(myAppCtx.getBean(TerminologyUploaderProviderDstu3.class)); + } + +} diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/resources/logback.xml b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/resources/logback.xml new file mode 100644 index 00000000000..ffec8d30c06 --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + INFO + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%file:%line] %msg%n + + + + + + + + \ No newline at end of file diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/templates/about.html b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/templates/about.html new file mode 100644 index 00000000000..d552027e956 --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/templates/about.html @@ -0,0 +1,67 @@ + + + + About This Server + + + +
+
+ +
+
+
+ +
+ +
+ +
+
+

About This Server

+
+
+
+ +
+

+ This server provides a nearly complete implementation of the FHIR Specification + using a 100% open source software stack. It is hosted by University Health Network. +

+

+ The architecture in use here is shown in the image on the right. This server is built + from a number of modules of the + HAPI FHIR + project, which is a 100% open-source (Apache 2.0 Licensed) Java based + implementation of the FHIR specification. +

+

+ +

+
+
+
+
+

Data On This Server

+
+
+

+ This server is regularly loaded with a standard set of test data sourced + from UHN's own testing environment. Do not use this server to store any data + that you will need later, as we will be regularly resetting it. +

+

+ This is not a production server and it provides no privacy. Do not store any + confidential data here. +

+
+
+ +
+
+
+ +
+
+ + diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/templates/tmpl-footer.html b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/templates/tmpl-footer.html new file mode 100644 index 00000000000..bf18c498a78 --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/templates/tmpl-footer.html @@ -0,0 +1,16 @@ + + +
+ +
+ diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/templates/tmpl-home-welcome.html b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/templates/tmpl-home-welcome.html new file mode 100644 index 00000000000..dfc4769d6e4 --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/templates/tmpl-home-welcome.html @@ -0,0 +1,52 @@ + + +
+ +

+ This is the home for the FHIR test server operated by + University Health Network. This server + (and the testing application you are currently using to access it) + is entirely built using + HAPI-FHIR, + a 100% open-source Java implementation of the + FHIR specification. +

+

+ Here are some things you might wish to try: +

+
    +
  • + View a + list of patients + on this server. +
  • +
  • + Construct a + search query + on this server. +
  • +
  • + Access a + different server + (use the Server menu at the top of the page to see a list of public FHIR servers) +
  • +
+
+ +

+ You are accessing the public FHIR server + . This server is hosted elsewhere on the internet + but is being accessed using the HAPI client implementation. +

+
+

+ + + This is not a production server! + + Do not store any information here that contains personal health information + or any other confidential information. This server will be regularly purged + and reloaded with fixed test data. +

+
+ diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/web.xml b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000000..882a7be5652 --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,108 @@ + + + + org.springframework.web.context.ContextLoaderListener + + + contextClass + + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + + contextConfigLocation + + ca.uhn.fhir.jpa.demo.FhirServerConfig + + + + + + + spring + org.springframework.web.servlet.DispatcherServlet + + contextClass + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + contextConfigLocation + ca.uhn.fhir.jpa.demo.FhirTesterConfig + + 2 + + + + fhirServlet + ca.uhn.fhir.jpa.demo.JpaServerDemo + + ImplementationDescription + FHIR JPA Server + + + FhirVersion + DSTU2 + + 1 + + + + fhirServlet + /baseDstu3/* + + + + spring + / + + + + + + + CORS Filter + org.ebaysf.web.cors.CORSFilter + + A comma separated list of allowed origins. Note: An '*' cannot be used for an allowed origin when using credentials. + cors.allowed.origins + * + + + A comma separated list of HTTP verbs, using which a CORS request can be made. + cors.allowed.methods + GET,POST,PUT,DELETE,OPTIONS + + + A comma separated list of allowed headers when making a non simple CORS request. + cors.allowed.headers + X-FHIR-Starter,Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers + + + A comma separated list non-standard response headers that will be exposed to XHR2 object. + cors.exposed.headers + Location,Content-Location + + + A flag that suggests if CORS is supported with cookies + cors.support.credentials + true + + + A flag to control logging + cors.logging.enabled + true + + + Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache. + cors.preflight.maxage + 300 + + + + CORS Filter + /* + + + + diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/javaee_6.xsd b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/javaee_6.xsd new file mode 100644 index 00000000000..9fb587749ce --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/javaee_6.xsd @@ -0,0 +1,2419 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2009 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + + The following definitions that appear in the common + shareable schema(s) of Java EE deployment descriptors should be + interpreted with respect to the context they are included: + + Deployment Component may indicate one of the following: + java ee application; + application client; + web application; + enterprise bean; + resource adapter; + + Deployment File may indicate one of the following: + ear file; + war file; + jar file; + rar file; + + + + + + + + + + + This group keeps the usage of the contained description related + elements consistent across Java EE deployment descriptors. + + All elements may occur multiple times with different languages, + to support localization of the content. + + + + + + + + + + + + + + + This group keeps the usage of the contained JNDI environment + reference elements consistent across Java EE deployment descriptors. + + + + + + + + + + + + + + + + + + + + + + + This group collects elements that are common to most + JNDI resource elements. + + + + + + + + + + The JNDI name to be looked up to resolve a resource reference. + + + + + + + + + + + + This group collects elements that are common to all the + JNDI resource elements. It does not include the lookup-name + element, that is only applicable to some resource elements. + + + + + + + + + A product specific name that this resource should be + mapped to. The name of this resource, as defined by the + resource's name element or defaulted, is a name that is + local to the application component using the resource. + (It's a name in the JNDI java:comp/env namespace.) Many + application servers provide a way to map these local + names to names of resources known to the application + server. This mapped name is often a global JNDI name, + but may be a name of any form. + + Application servers are not required to support any + particular form or type of mapped name, nor the ability + to use mapped names. The mapped name is + product-dependent and often installation-dependent. No + use of a mapped name is portable. + + + + + + + + + + + + + + + + Configuration of a DataSource. + + + + + + + + + Description of this DataSource. + + + + + + + + + The name element specifies the JNDI name of the + data source being defined. + + + + + + + + + DataSource, XADataSource or ConnectionPoolDataSource + implementation class. + + + + + + + + + Database server name. + + + + + + + + + Port number where a server is listening for requests. + + + + + + + + + Name of a database on a server. + + + + + + + + url property is specified + along with other standard DataSource properties + such as serverName, databaseName + and portNumber, the more specific properties will + take precedence and url will be ignored. + + ]]> + + + + + + + + User name to use for connection authentication. + + + + + + + + + Password to use for connection authentication. + + + + + + + + + JDBC DataSource property. This may be a vendor-specific + property or a less commonly used DataSource property. + + + + + + + + + Sets the maximum time in seconds that this data source + will wait while attempting to connect to a database. + + + + + + + + + Set to false if connections should not participate in + transactions. + + + + + + + + + Isolation level for connections. + + + + + + + + + Number of connections that should be created when a + connection pool is initialized. + + + + + + + + + Maximum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + Minimum number of connections that should be concurrently + allocated for a connection pool. + + + + + + + + + The number of seconds that a physical connection should + remain unused in the pool before the connection is + closed for a connection pool. + + + + + + + + + The total number of statements that a connection pool + should keep open. + + + + + + + + + + + + + + + + The description type is used by a description element to + provide text describing the parent element. The elements + that use this type should include any information that the + Deployment Component's Deployment File file producer wants + to provide to the consumer of the Deployment Component's + Deployment File (i.e., to the Deployer). Typically, the + tools used by such a Deployment File consumer will display + the description when processing the parent element that + contains the description. + + The lang attribute defines the language that the + description is provided in. The default value is "en" (English). + + + + + + + + + + + + + + + This type defines a dewey decimal that is used + to describe versions of documents. + + + + + + + + + + + + + + + + Employee Self Service + + + The value of the xml:lang attribute is "en" (English) by default. + + ]]> + + + + + + + + + + + + + + + + EmployeeRecord + + ../products/product.jar#ProductEJB + + ]]> + + + + + + + + + + + + + + + The ejb-local-refType is used by ejb-local-ref elements for + the declaration of a reference to an enterprise bean's local + home or to the local business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the EJB reference name used in the code of the Deployment + Component that's referencing the enterprise bean. + - the optional expected type of the referenced enterprise bean + - the optional expected local interface of the referenced + enterprise bean or the local business interface of the + referenced enterprise bean. + - the optional expected local home interface of the referenced + enterprise bean. Not applicable if this ejb-local-ref refers + to the local business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property. + + + + + + + + + + + + + + + + + + + + + + ejb/Payroll + + ]]> + + + + + + + + + + + + + + + The ejb-refType is used by ejb-ref elements for the + declaration of a reference to an enterprise bean's home or + to the remote business interface of a 3.0 bean. + The declaration consists of: + + - an optional description + - the EJB reference name used in the code of + the Deployment Component that's referencing the enterprise + bean. + - the optional expected type of the referenced enterprise bean + - the optional remote interface of the referenced enterprise bean + or the remote business interface of the referenced enterprise + bean + - the optional expected home interface of the referenced + enterprise bean. Not applicable if this ejb-ref + refers to the remote business interface of a 3.0 bean. + - optional ejb-link information, used to specify the + referenced enterprise bean + - optional elements to define injection of the named enterprise + bean into a component field or property + + + + + + + + + + + + + + + + + + + + + + + The ejb-ref-typeType contains the expected type of the + referenced enterprise bean. + + The ejb-ref-type designates a value + that must be one of the following: + + Entity + Session + + + + + + + + + + + + + + + + + + + This type is used to designate an empty + element when used. + + + + + + + + + + + + + + The env-entryType is used to declare an application's + environment entry. The declaration consists of an optional + description, the name of the environment entry, a type + (optional if the value is injected, otherwise required), and + an optional value. + + It also includes optional elements to define injection of + the named resource into fields or JavaBeans properties. + + If a value is not specified and injection is requested, + no injection will occur and no entry of the specified name + will be created. This allows an initial value to be + specified in the source code without being incorrectly + changed when no override has been specified. + + If a value is not specified and no injection is requested, + a value must be supplied during deployment. + + This type is used by env-entry elements. + + + + + + + + + minAmount + + ]]> + + + + + + + java.lang.Integer + + ]]> + + + + + + + 100.00 + + ]]> + + + + + + + + + + + + + + + java.lang.Boolean + java.lang.Class + com.example.Color + + ]]> + + + + + + + + + + + + + + + The elements that use this type designate the name of a + Java class or interface. The name is in the form of a + "binary name", as defined in the JLS. This is the form + of name used in Class.forName(). Tools that need the + canonical name (the name used in source code) will need + to convert this binary name to the canonical name. + + + + + + + + + + + + + + + + This type defines four different values which can designate + boolean values. This includes values yes and no which are + not designated by xsd:boolean + + + + + + + + + + + + + + + + + + + + + The icon type contains small-icon and large-icon elements + that specify the file names for small and large GIF, JPEG, + or PNG icon images used to represent the parent element in a + GUI tool. + + The xml:lang attribute defines the language that the + icon file names are provided in. Its value is "en" (English) + by default. + + + + + + + + employee-service-icon16x16.jpg + + ]]> + + + + + + + employee-service-icon32x32.jpg + + ]]> + + + + + + + + + + + + + + + + An injection target specifies a class and a name within + that class into which a resource should be injected. + + The injection target class specifies the fully qualified + class name that is the target of the injection. The + Java EE specifications describe which classes can be an + injection target. + + The injection target name specifies the target within + the specified class. The target is first looked for as a + JavaBeans property name. If not found, the target is + looked for as a field name. + + The specified resource will be injected into the target + during initialization of the class by either calling the + set method for the target property or by setting a value + into the named field. + + + + + + + + + + + + + + The following transaction isolation levels are allowed + (see documentation for the java.sql.Connection interface): + TRANSACTION_READ_UNCOMMITTED + TRANSACTION_READ_COMMITTED + TRANSACTION_REPEATABLE_READ + TRANSACTION_SERIALIZABLE + + + + + + + + + + + + + + + + + + + The java-identifierType defines a Java identifier. + The users of this type should further verify that + the content does not contain Java reserved keywords. + + + + + + + + + + + + + + + + + + This is a generic type that designates a Java primitive + type or a fully qualified name of a Java interface/type, + or an array of such types. + + + + + + + + + + + + + + + + + : + + Example: + + jdbc:mysql://localhost:3307/testdb + + ]]> + + + + + + + + + + + + + + + + + The jndi-nameType type designates a JNDI name in the + Deployment Component's environment and is relative to the + java:comp/env context. A JNDI name must be unique within the + Deployment Component. + + + + + + + + + + + + + + + com.aardvark.payroll.PayrollHome + + ]]> + + + + + + + + + + + + + + + The lifecycle-callback type specifies a method on a + class to be called when a lifecycle event occurs. + Note that each class may have only one lifecycle callback + method for any given event and that the method may not + be overloaded. + + If the lifefycle-callback-class element is missing then + the class defining the callback is assumed to be the + component class in scope at the place in the descriptor + in which the callback definition appears. + + + + + + + + + + + + + + + + + The listenerType indicates the deployment properties for a web + application listener bean. + + + + + + + + + + The listener-class element declares a class in the + application must be registered as a web + application listener bean. The value is the fully + qualified classname of the listener class. + + + + + + + + + + + + + + + + The localType defines the fully-qualified name of an + enterprise bean's local interface. + + + + + + + + + + + + + + + + The local-homeType defines the fully-qualified + name of an enterprise bean's local home interface. + + + + + + + + + + + + + + + + This type is a general type that can be used to declare + parameter/value lists. + + + + + + + + + + The param-name element contains the name of a + parameter. + + + + + + + + + The param-value element contains the value of a + parameter. + + + + + + + + + + + + + + + + The elements that use this type designate either a relative + path or an absolute path starting with a "/". + + In elements that specify a pathname to a file within the + same Deployment File, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the Deployment File's namespace. Absolute filenames (i.e., + those starting with "/") also specify names in the root of + the Deployment File's namespace. In general, relative names + are preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + + myPersistenceContext + + + + + myPersistenceContext + + PersistenceUnit1 + + Extended + + + ]]> + + + + + + + + + The persistence-context-ref-name element specifies + the name of a persistence context reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Java EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + Used to specify properties for the container or persistence + provider. Vendor-specific properties may be included in + the set of properties. Properties that are not recognized + by a vendor must be ignored. Entries that make use of the + namespace javax.persistence and its subnamespaces must not + be used for vendor-specific properties. The namespace + javax.persistence is reserved for use by the specification. + + + + + + + + + + + + + + + + + The persistence-context-typeType specifies the transactional + nature of a persistence context reference. + + The value of the persistence-context-type element must be + one of the following: + Transaction + Extended + + + + + + + + + + + + + + + + + + + Specifies a name/value pair. + + + + + + + + + + + + + + + + + + + + myPersistenceUnit + + + + + myPersistenceUnit + + PersistenceUnit1 + + + + ]]> + + + + + + + + + The persistence-unit-ref-name element specifies + the name of a persistence unit reference; its + value is the environment entry name used in + Deployment Component code. The name is a JNDI name + relative to the java:comp/env context. + + + + + + + + + The Application Assembler(or BeanProvider) may use the + following syntax to avoid the need to rename persistence + units to have unique names within a Java EE application. + + The Application Assembler specifies the pathname of the + root of the persistence.xml file for the referenced + persistence unit and appends the name of the persistence + unit separated from the pathname by #. The pathname is + relative to the referencing application component jar file. + In this manner, multiple persistence units with the same + persistence unit name may be uniquely identified when the + Application Assembler cannot change persistence unit names. + + + + + + + + + + + + + + + + com.wombat.empl.EmployeeService + + ]]> + + + + + + + + + + + + + + + jms/StockQueue + + javax.jms.Queue + + + + ]]> + + + + + + + + + The resource-env-ref-name element specifies the name + of a resource environment reference; its value is + the environment entry name used in + the Deployment Component code. The name is a JNDI + name relative to the java:comp/env context and must + be unique within a Deployment Component. + + + + + + + + + The resource-env-ref-type element specifies the type + of a resource environment reference. It is the + fully qualified name of a Java language class or + interface. + + + + + + + + + + + + + + + + + jdbc/EmployeeAppDB + javax.sql.DataSource + Container + Shareable + + + ]]> + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. + The name is a JNDI name relative to the + java:comp/env context. + The name must be unique within a Deployment File. + + + + + + + + + The res-type element specifies the type of the data + source. The type is specified by the fully qualified + Java language class or interface + expected to be implemented by the data source. + + + + + + + + + + + + + + + + + + + The res-authType specifies whether the Deployment Component + code signs on programmatically to the resource manager, or + whether the Container will sign on to the resource manager + on behalf of the Deployment Component. In the latter case, + the Container uses information that is supplied by the + Deployer. + + The value must be one of the two following: + + Application + Container + + + + + + + + + + + + + + + + + + + The res-sharing-scope type specifies whether connections + obtained through the given resource manager connection + factory reference can be shared. The value, if specified, + must be one of the two following: + + Shareable + Unshareable + + The default value is Shareable. + + + + + + + + + + + + + + + + + + + The run-asType specifies the run-as identity to be + used for the execution of a component. It contains an + optional description, and the name of a security role. + + + + + + + + + + + + + + + + + + The role-nameType designates the name of a security role. + + The name must conform to the lexical rules for a token. + + + + + + + + + + + + + + + + + This role includes all employees who are authorized + to access the employee service application. + + employee + + + ]]> + + + + + + + + + + + + + + + + + The security-role-refType contains the declaration of a + security role reference in a component's or a + Deployment Component's code. The declaration consists of an + optional description, the security role name used in the + code, and an optional link to a security role. If the + security role is not specified, the Deployer must choose an + appropriate security role. + + + + + + + + + + The value of the role-name element must be the String used + as the parameter to the + EJBContext.isCallerInRole(String roleName) method or the + HttpServletRequest.isUserInRole(String role) method. + + + + + + + + + The role-link element is a reference to a defined + security role. The role-link element must contain + the name of one of the security roles defined in the + security-role elements. + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:QName. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:boolean. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:NMTOKEN. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:anyURI. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:integer. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:positiveInteger. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:nonNegativeInteger. + + + + + + + + + + + + + + + + + + This type adds an "id" attribute to xsd:string. + + + + + + + + + + + + + + + + + + This is a special string datatype that is defined by Java EE as + a base type for defining collapsed strings. When schemas + require trailing/leading space elimination as well as + collapsing the existing whitespace, this base type may be + used. + + + + + + + + + + + + + + + + + + This simple type designates a boolean with only two + permissible values + + - true + - false + + + + + + + + + + + + + + + + + + The url-patternType contains the url pattern of the mapping. + It must follow the rules specified in Section 11.2 of the + Servlet API Specification. This pattern is assumed to be in + URL-decoded form and must not contain CR(#xD) or LF(#xA). + If it contains those characters, the container must inform + the developer with a descriptive error message. + The container must preserve all characters including whitespaces. + + + + + + + + + + + + + + + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-name element specifies a + name for a message destination. This name must be + unique among the names of message destinations + within the Deployment File. + + + + + + + + + A product specific name that this message destination + should be mapped to. Each message-destination-ref + element that references this message destination will + define a name in the namespace of the referencing + component or in one of the other predefined namespaces. + Many application servers provide a way to map these + local names to names of resources known to the + application server. This mapped name is often a global + JNDI name, but may be a name of any form. Each of the + local names should be mapped to this same global name. + + Application servers are not required to support any + particular form or type of mapped name, nor the ability + to use mapped names. The mapped name is + product-dependent and often installation-dependent. No + use of a mapped name is portable. + + + + + + + + + The JNDI name to be looked up to resolve the message destination. + + + + + + + + + + + + + + + + jms/StockQueue + + javax.jms.Queue + + Consumes + + CorporateStocks + + + + ]]> + + + + + + + + + The message-destination-ref-name element specifies + the name of a message destination reference; its + value is the environment entry name used in + Deployment Component code. + + + + + + + + + + + + + + + + + + + + The message-destination-usageType specifies the use of the + message destination indicated by the reference. The value + indicates whether messages are consumed from the message + destination, produced for the destination, or both. The + Assembler makes use of this information in linking producers + of a destination with its consumers. + + The value of the message-destination-usage element must be + one of the following: + Consumes + Produces + ConsumesProduces + + + + + + + + + + + + + + + + + + + javax.jms.Queue + + + ]]> + + + + + + + + + + + + + + + The message-destination-linkType is used to link a message + destination reference or message-driven bean to a message + destination. + + The Assembler sets the value to reflect the flow of messages + between producers and consumers in the application. + + The value must be the message-destination-name of a message + destination in the same Deployment File or in another + Deployment File in the same Java EE application unit. + + Alternatively, the value may be composed of a path name + specifying a Deployment File containing the referenced + message destination with the message-destination-name of the + destination appended and separated from the path name by + "#". The path name is relative to the Deployment File + containing Deployment Component that is referencing the + message destination. This allows multiple message + destinations with the same name to be uniquely identified. + + + + + + + + + diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/jsp_2_2.xsd b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/jsp_2_2.xsd new file mode 100644 index 00000000000..fa41e4266f1 --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/jsp_2_2.xsd @@ -0,0 +1,389 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2009 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + + This is the XML Schema for the JSP 2.2 deployment descriptor + types. The JSP 2.2 schema contains all the special + structures and datatypes that are necessary to use JSP files + from a web application. + + The contents of this schema is used by the web-common_3_0.xsd + file to define JSP specific content. + + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The jsp-configType is used to provide global configuration + information for the JSP files in a web application. It has + two subelements, taglib and jsp-property-group. + + + + + + + + + + + + + + + + + + The jsp-file element contains the full path to a JSP file + within the web application beginning with a `/'. + + + + + + + + + + + + + + + + The jsp-property-groupType is used to group a number of + files so they can be given global property information. + All files so described are deemed to be JSP files. The + following additional properties can be described: + + - Control whether EL is ignored. + - Control whether scripting elements are invalid. + - Indicate pageEncoding information. + - Indicate that a resource is a JSP document (XML). + - Prelude and Coda automatic includes. + - Control whether the character sequence #{ is allowed + when used as a String literal. + - Control whether template text containing only + whitespaces must be removed from the response output. + - Indicate the default contentType information. + - Indicate the default buffering model for JspWriter + - Control whether error should be raised for the use of + undeclared namespaces in a JSP page. + + + + + + + + + + + Can be used to easily set the isELIgnored + property of a group of JSP pages. By default, the + EL evaluation is enabled for Web Applications using + a Servlet 2.4 or greater web.xml, and disabled + otherwise. + + + + + + + + + The valid values of page-encoding are those of the + pageEncoding page directive. It is a + translation-time error to name different encodings + in the pageEncoding attribute of the page directive + of a JSP page and in a JSP configuration element + matching the page. It is also a translation-time + error to name different encodings in the prolog + or text declaration of a document in XML syntax and + in a JSP configuration element matching the document. + It is legal to name the same encoding through + mulitple mechanisms. + + + + + + + + + Can be used to easily disable scripting in a + group of JSP pages. By default, scripting is + enabled. + + + + + + + + + If true, denotes that the group of resources + that match the URL pattern are JSP documents, + and thus must be interpreted as XML documents. + If false, the resources are assumed to not + be JSP documents, unless there is another + property group that indicates otherwise. + + + + + + + + + The include-prelude element is a context-relative + path that must correspond to an element in the + Web Application. When the element is present, + the given path will be automatically included (as + in an include directive) at the beginning of each + JSP page in this jsp-property-group. + + + + + + + + + The include-coda element is a context-relative + path that must correspond to an element in the + Web Application. When the element is present, + the given path will be automatically included (as + in an include directive) at the end of each + JSP page in this jsp-property-group. + + + + + + + + + The character sequence #{ is reserved for EL expressions. + Consequently, a translation error occurs if the #{ + character sequence is used as a String literal, unless + this element is enabled (true). Disabled (false) by + default. + + + + + + + + + Indicates that template text containing only whitespaces + must be removed from the response output. It has no + effect on JSP documents (XML syntax). Disabled (false) + by default. + + + + + + + + + The valid values of default-content-type are those of the + contentType page directive. It specifies the default + response contentType if the page directive does not include + a contentType attribute. + + + + + + + + + The valid values of buffer are those of the + buffer page directive. It specifies if buffering should be + used for the output to response, and if so, the size of the + buffer to use. + + + + + + + + + The default behavior when a tag with unknown namespace is used + in a JSP page (regular syntax) is to silently ignore it. If + set to true, then an error must be raised during the translation + time when an undeclared tag is used in a JSP page. Disabled + (false) by default. + + + + + + + + + + + + + + + + The taglibType defines the syntax for declaring in + the deployment descriptor that a tag library is + available to the application. This can be done + to override implicit map entries from TLD files and + from the container. + + + + + + + + + A taglib-uri element describes a URI identifying a + tag library used in the web application. The body + of the taglib-uri element may be either an + absolute URI specification, or a relative URI. + There should be no entries in web.xml with the + same taglib-uri value. + + + + + + + + + the taglib-location element contains the location + (as a resource relative to the root of the web + application) where to find the Tag Library + Description file for the tag library. + + + + + + + + + diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/web-app_3_0.xsd b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/web-app_3_0.xsd new file mode 100644 index 00000000000..bbcdf43cd3a --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/web-app_3_0.xsd @@ -0,0 +1,272 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2009 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Java EE + namespace with the following location: + + http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd + + ]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + The web-app element is the root of the deployment + descriptor for a web application. Note that the sub-elements + of this element can be in the arbitrary order. Because of + that, the multiplicity of the elements of distributable, + session-config, welcome-file-list, jsp-config, login-config, + and locale-encoding-mapping-list was changed from "?" to "*" + in this schema. However, the deployment descriptor instance + file must not contain multiple elements of session-config, + jsp-config, and login-config. When there are multiple elements of + welcome-file-list or locale-encoding-mapping-list, the container + must concatenate the element contents. The multiple occurence + of the element distributable is redundant and the container + treats that case exactly in the same way when there is only + one distributable. + + + + + + + + The servlet element contains the name of a servlet. + The name must be unique within the web application. + + + + + + + + + + + The filter element contains the name of a filter. + The name must be unique within the web application. + + + + + + + + + + + The ejb-local-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The ejb-ref-name element contains the name of an EJB + reference. The EJB reference is an entry in the web + application's environment and is relative to the + java:comp/env context. The name must be unique within + the web application. + + It is recommended that name is prefixed with "ejb/". + + + + + + + + + + + The resource-env-ref-name element specifies the name of + a resource environment reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The message-destination-ref-name element specifies the name of + a message destination reference; its value is the + environment entry name used in the web application code. + The name is a JNDI name relative to the java:comp/env + context and must be unique within a web application. + + + + + + + + + + + The res-ref-name element specifies the name of a + resource manager connection factory reference. The name + is a JNDI name relative to the java:comp/env context. + The name must be unique within a web application. + + + + + + + + + + + The env-entry-name element contains the name of a web + application's environment entry. The name is a JNDI + name relative to the java:comp/env context. The name + must be unique within a web application. + + + + + + + + + + + A role-name-key is specified to allow the references + from the security-role-refs. + + + + + + + + + + + The keyref indicates the references from + security-role-ref to a specified role-name. + + + + + + + + + diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/web-common_3_0.xsd b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/web-common_3_0.xsd new file mode 100644 index 00000000000..f994bc2c651 --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/web-common_3_0.xsd @@ -0,0 +1,1575 @@ + + + + + + DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + + Copyright 2003-2009 Sun Microsystems, Inc. All rights reserved. + + The contents of this file are subject to the terms of either the + GNU General Public License Version 2 only ("GPL") or the Common + Development and Distribution License("CDDL") (collectively, the + "License"). You may not use this file except in compliance with + the License. You can obtain a copy of the License at + https://glassfish.dev.java.net/public/CDDL+GPL.html or + glassfish/bootstrap/legal/LICENSE.txt. See the License for the + specific language governing permissions and limitations under the + License. + + When distributing the software, include this License Header + Notice in each file and include the License file at + glassfish/bootstrap/legal/LICENSE.txt. Sun designates this + particular file as subject to the "Classpath" exception as + provided by Sun in the GPL Version 2 section of the License file + that accompanied this code. If applicable, add the following + below the License Header, with the fields enclosed by brackets [] + replaced by your own identifying information: + "Portions Copyrighted [year] [name of copyright owner]" + + Contributor(s): + + If you wish your version of this file to be governed by only the + CDDL or only the GPL Version 2, indicate your decision by adding + "[Contributor] elects to include this software in this + distribution under the [CDDL or GPL Version 2] license." If you + don't indicate a single choice of license, a recipient has the + option to distribute your version of this file under either the + CDDL, the GPL Version 2 or to extend the choice of license to its + licensees as provided above. However, if you add GPL Version 2 + code and therefore, elected the GPL Version 2 license, then the + option applies only if the new code is made subject to such + option by the copyright holder. + + + + + + + + ... + + + The instance documents may indicate the published version of + the schema using the xsi:schemaLocation attribute for Java EE + namespace with the following location: + + http://java.sun.com/xml/ns/javaee/web-common_3_0.xsd + + ]]> + + + + + + + The following conventions apply to all Java EE + deployment descriptor elements unless indicated otherwise. + + - In elements that specify a pathname to a file within the + same JAR file, relative filenames (i.e., those not + starting with "/") are considered relative to the root of + the JAR file's namespace. Absolute filenames (i.e., those + starting with "/") also specify names in the root of the + JAR file's namespace. In general, relative names are + preferred. The exception is .war files where absolute + names are preferred for consistency with the Servlet API. + + + + + + + + + + + + + + + + + The context-param element contains the declaration + of a web application's servlet context + initialization parameters. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The metadata-complete attribute defines whether this + deployment descriptor and other related deployment + descriptors for this module (e.g., web service + descriptors) are complete, or whether the class + files available to this module and packaged with + this application should be examined for annotations + that specify deployment information. + + If metadata-complete is set to "true", the deployment + tool must ignore any annotations that specify deployment + information, which might be present in the class files + of the application. + + If metadata-complete is not specified or is set to + "false", the deployment tool must examine the class + files of the application for annotations, as + specified by the specifications. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The auth-constraintType indicates the user roles that + should be permitted access to this resource + collection. The role-name used here must either correspond + to the role-name of one of the security-role elements + defined for this web application, or be the specially + reserved role-name "*" that is a compact syntax for + indicating all roles in the web application. If both "*" + and rolenames appear, the container interprets this as all + roles. If no roles are defined, no user is allowed access + to the portion of the web application described by the + containing security-constraint. The container matches + role names case sensitively when determining access. + + + + + + + + + + + + + + + + + + The auth-methodType is used to configure the authentication + mechanism for the web application. As a prerequisite to + gaining access to any web resources which are protected by + an authorization constraint, a user must have authenticated + using the configured mechanism. Legal values are "BASIC", + "DIGEST", "FORM", "CLIENT-CERT", or a vendor-specific + authentication scheme. + + Used in: login-config + + + + + + + + + + + + + + + + The dispatcher has five legal values: FORWARD, REQUEST, + INCLUDE, ASYNC, and ERROR. + + A value of FORWARD means the Filter will be applied under + RequestDispatcher.forward() calls. + A value of REQUEST means the Filter will be applied under + ordinary client calls to the path or servlet. + A value of INCLUDE means the Filter will be applied under + RequestDispatcher.include() calls. + A value of ASYNC means the Filter will be applied under + calls dispatched from an AsyncContext. + A value of ERROR means the Filter will be applied under the + error page mechanism. + + The absence of any dispatcher elements in a filter-mapping + indicates a default of applying filters only under ordinary + client calls to the path or servlet. + + + + + + + + + + + + + + + + + + + + + + The error-code contains an HTTP error code, ex: 404 + + Used in: error-page + + + + + + + + + + + + + + + + + + + The error-pageType contains a mapping between an error code + or exception type to the path of a resource in the web + application. + + Error-page declarations using the exception-type element in + the deployment descriptor must be unique up to the class name of + the exception-type. Similarly, error-page declarations using the + status-code element must be unique in the deployment descriptor + up to the status code. + + Used in: web-app + + + + + + + + + + + The exception-type contains a fully qualified class + name of a Java exception type. + + + + + + + + + + The location element contains the location of the + resource in the web application relative to the root of + the web application. The value of the location must have + a leading `/'. + + + + + + + + + + + + + + + + The filterType is used to declare a filter in the web + application. The filter is mapped to either a servlet or a + URL pattern in the filter-mapping element, using the + filter-name value to reference. Filters can access the + initialization parameters declared in the deployment + descriptor at runtime via the FilterConfig interface. + + Used in: web-app + + + + + + + + + + + The fully qualified classname of the filter. + + + + + + + + + + The init-param element contains a name/value pair as + an initialization param of a servlet filter + + + + + + + + + + + + + + + + Declaration of the filter mappings in this web + application is done by using filter-mappingType. + The container uses the filter-mapping + declarations to decide which filters to apply to a request, + and in what order. The container matches the request URI to + a Servlet in the normal way. To determine which filters to + apply it matches filter-mapping declarations either on + servlet-name, or on url-pattern for each filter-mapping + element, depending on which style is used. The order in + which filters are invoked is the order in which + filter-mapping declarations that match a request URI for a + servlet appear in the list of filter-mapping elements.The + filter-name value must be the value of the filter-name + sub-elements of one of the filter declarations in the + deployment descriptor. + + + + + + + + + + + + + + + + + + + + + + This type defines a string which contains at least one + character. + + + + + + + + + + + + + + + + + + The logical name of the filter is declare + by using filter-nameType. This name is used to map the + filter. Each filter name is unique within the web + application. + + Used in: filter, filter-mapping + + + + + + + + + + + + + + + + The form-login-configType specifies the login and error + pages that should be used in form based login. If form based + authentication is not used, these elements are ignored. + + Used in: login-config + + + + + + + + + The form-login-page element defines the location in the web + app where the page that can be used for login can be + found. The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + The form-error-page element defines the location in + the web app where the error page that is displayed + when login is not successful can be found. + The path begins with a leading / and is interpreted + relative to the root of the WAR. + + + + + + + + + + + + + A HTTP method type as defined in HTTP 1.1 section 2.2. + + + + + + + + + + + + + + + + + + + + + + + + + + The login-configType is used to configure the authentication + method that should be used, the realm name that should be + used for this application, and the attributes that are + needed by the form login mechanism. + + Used in: web-app + + + + + + + + + + The realm name element specifies the realm name to + use in HTTP Basic authorization. + + + + + + + + + + + + + + + + + The mime-mappingType defines a mapping between an extension + and a mime type. + + Used in: web-app + + + + + + + + The extension element contains a string describing an + extension. example: "txt" + + + + + + + + + + + + + + + + + The mime-typeType is used to indicate a defined mime type. + + Example: + "text/plain" + + Used in: mime-mapping + + + + + + + + + + + + + + + + + + The security-constraintType is used to associate + security constraints with one or more web resource + collections + + Used in: web-app + + + + + + + + + + + + + + + + + + + + The servletType is used to declare a servlet. + It contains the declarative data of a + servlet. If a jsp-file is specified and the load-on-startup + element is present, then the JSP should be precompiled and + loaded. + + Used in: web-app + + + + + + + + + + + + The servlet-class element contains the fully + qualified class name of the servlet. + + + + + + + + + + + + The load-on-startup element indicates that this + servlet should be loaded (instantiated and have + its init() called) on the startup of the web + application. The optional contents of these + element must be an integer indicating the order in + which the servlet should be loaded. If the value + is a negative integer, or the element is not + present, the container is free to load the servlet + whenever it chooses. If the value is a positive + integer or 0, the container must load and + initialize the servlet as the application is + deployed. The container must guarantee that + servlets marked with lower integers are loaded + before servlets marked with higher integers. The + container may choose the order of loading of + servlets with the same load-on-start-up value. + + + + + + + + + + + + + + + + + + + + + The servlet-mappingType defines a mapping between a + servlet and a url pattern. + + Used in: web-app + + + + + + + + + + + + + + + + + + The servlet-name element contains the canonical name of the + servlet. Each servlet name is unique within the web + application. + + + + + + + + + + + + + + + + The session-configType defines the session parameters + for this web application. + + Used in: web-app + + + + + + + + + The session-timeout element defines the default + session timeout interval for all sessions created + in this web application. The specified timeout + must be expressed in a whole number of minutes. + If the timeout is 0 or less, the container ensures + the default behaviour of sessions is never to time + out. If this element is not specified, the container + must set its default timeout period. + + + + + + + + + The cookie-config element defines the configuration of the + session tracking cookies created by this web application. + + + + + + + + + The tracking-mode element defines the tracking modes + for sessions created by this web application + + + + + + + + + + + + + + + + The cookie-configType defines the configuration for the + session tracking cookies of this web application. + + Used in: session-config + + + + + + + + + The name that will be assigned to any session tracking + cookies created by this web application. + The default is JSESSIONID + + + + + + + + + The domain name that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + The path that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + The comment that will be assigned to any session tracking + cookies created by this web application. + + + + + + + + + Specifies whether any session tracking cookies created + by this web application will be marked as HttpOnly + + + + + + + + + Specifies whether any session tracking cookies created + by this web application will be marked as secure + even if the request that initiated the corresponding session + is using plain HTTP instead of HTTPS + + + + + + + + + The lifetime (in seconds) that will be assigned to any + session tracking cookies created by this web application. + Default is -1 + + + + + + + + + + + + + + + + The name that will be assigned to any session tracking + cookies created by this web application. + The default is JSESSIONID + + Used in: cookie-config + + + + + + + + + + + + + + + + The domain name that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The path that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The comment that will be assigned to any session tracking + cookies created by this web application. + + Used in: cookie-config + + + + + + + + + + + + + + + + The tracking modes for sessions created by this web + application + + Used in: session-config + + + + + + + + + + + + + + + + + + + + The transport-guaranteeType specifies that the communication + between client and server should be NONE, INTEGRAL, or + CONFIDENTIAL. NONE means that the application does not + require any transport guarantees. A value of INTEGRAL means + that the application requires that the data sent between the + client and server be sent in such a way that it can't be + changed in transit. CONFIDENTIAL means that the application + requires that the data be transmitted in a fashion that + prevents other entities from observing the contents of the + transmission. In most cases, the presence of the INTEGRAL or + CONFIDENTIAL flag will indicate that the use of SSL is + required. + + Used in: user-data-constraint + + + + + + + + + + + + + + + + + + + + The user-data-constraintType is used to indicate how + data communicated between the client and container should be + protected. + + Used in: security-constraint + + + + + + + + + + + + + + + + + + The elements that use this type designate a path starting + with a "/" and interpreted relative to the root of a WAR + file. + + + + + + + + + + + + + + + This type contains the recognized versions of + web-application supported. It is used to designate the + version of the web application. + + + + + + + + + + + + + + + + The web-resource-collectionType is used to identify the + resources and HTTP methods on those resources to which a + security constraint applies. If no HTTP methods are specified, + then the security constraint applies to all HTTP methods. + If HTTP methods are specified by http-method-omission + elements, the security constraint applies to all methods + except those identified in the collection. + http-method-omission and http-method elements are never + mixed in the same collection. + + Used in: security-constraint + + + + + + + + + The web-resource-name contains the name of this web + resource collection. + + + + + + + + + + + + Each http-method names an HTTP method to which the + constraint applies. + + + + + + + + + Each http-method-omission names an HTTP method to + which the constraint does not apply. + + + + + + + + + + + + + + + + + The welcome-file-list contains an ordered list of welcome + files elements. + + Used in: web-app + + + + + + + + + The welcome-file element contains file name to use + as a default welcome file, such as index.html + + + + + + + + + + + + + The localeType defines valid locale defined by ISO-639-1 + and ISO-3166. + + + + + + + + + + + + + The encodingType defines IANA character sets. + + + + + + + + + + + + + + + + The locale-encoding-mapping-list contains one or more + locale-encoding-mapping(s). + + + + + + + + + + + + + + + + + The locale-encoding-mapping contains locale name and + encoding name. The locale name must be either "Language-code", + such as "ja", defined by ISO-639 or "Language-code_Country-code", + such as "ja_JP". "Country code" is defined by ISO-3166. + + + + + + + + + + + + + + + + + + This element indicates that the ordering sub-element in which + it was placed should take special action regarding the ordering + of this application resource relative to other application + configuration resources. + See section 8.2.2 of the specification for details. + + + + + + + + + + + + + + Please see section 8.2.2 of the specification for details. + + + + + + + + + + + + + + + + + Please see section 8.2.2 of the specification for details. + + + + + + + + + + + + + + + + + This element contains a sequence of "name" elements, each of + which + refers to an application configuration resource by the "name" + declared on its web.xml fragment. This element can also contain + a single "others" element which specifies that this document + comes + before or after other documents within the application. + See section 8.2.2 of the specification for details. + + + + + + + + + + + + + + + + + This element specifies configuration information related to the + handling of multipart/form-data requests. + + + + + + + + + The directory location where uploaded files will be stored + + + + + + + + + The maximum size limit of uploaded files + + + + + + + + + The maximum size limit of multipart/form-data requests + + + + + + + + + The size threshold after which an uploaded file will be + written to disk + + + + + + + + diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/xml.xsd b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/xml.xsd new file mode 100644 index 00000000000..aea7d0db0a4 --- /dev/null +++ b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/main/webapp/WEB-INF/xsd/xml.xsd @@ -0,0 +1,287 @@ + + + + + + +
+

About the XML namespace

+ +
+

+ This schema document describes the XML namespace, in a form + suitable for import by other schema documents. +

+

+ See + http://www.w3.org/XML/1998/namespace.html and + + http://www.w3.org/TR/REC-xml for information + about this namespace. +

+

+ Note that local names in this namespace are intended to be + defined only by the World Wide Web Consortium or its subgroups. + The names currently defined in this namespace are listed below. + They should not be used with conflicting semantics by any Working + Group, specification, or document instance. +

+

+ See further below in this document for more information about how to refer to this schema document from your own + XSD schema documents and about the + namespace-versioning policy governing this schema document. +

+
+
+
+
+ + + + +
+ +

lang (as an attribute name)

+

+ denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification.

+ +
+
+

Notes

+

+ Attempting to install the relevant ISO 2- and 3-letter + codes as the enumerated possible values is probably never + going to be a realistic possibility. +

+

+ See BCP 47 at + http://www.rfc-editor.org/rfc/bcp/bcp47.txt + and the IANA language subtag registry at + + http://www.iana.org/assignments/language-subtag-registry + for further information. +

+

+ The union allows for the 'un-declaration' of xml:lang with + the empty string. +

+
+
+
+ + + + + + + + + +
+ + + + +
+ +

space (as an attribute name)

+

+ denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification.

+ +
+
+
+ + + + + + +
+ + + +
+ +

base (as an attribute name)

+

+ denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification.

+ +

+ See http://www.w3.org/TR/xmlbase/ + for information about this attribute. +

+
+
+
+
+ + + + +
+ +

id (as an attribute name)

+

+ denotes an attribute whose value + should be interpreted as if declared to be of type ID. + This name is reserved by virtue of its definition in the + xml:id specification.

+ +

+ See http://www.w3.org/TR/xml-id/ + for information about this attribute. +

+
+
+
+
+ + + + + + + + + + +
+ +

Father (in any context at all)

+ +
+

+ denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: +

+
+

+ In appreciation for his vision, leadership and + dedication the W3C XML Plenary on this 10th day of + February, 2000, reserves for Jon Bosak in perpetuity + the XML name "xml:Father". +

+
+
+
+
+
+ + + +
+

About this schema document

+ +
+

+ This schema defines attributes and an attribute group suitable + for use by schemas wishing to allow xml:base, + xml:lang, xml:space or + xml:id attributes on elements they define. +

+

+ To enable this, such a schema must import this schema for + the XML namespace, e.g. as follows: +

+
+          <schema . . .>
+           . . .
+           <import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
+     
+

+ or +

+
+           <import namespace="http://www.w3.org/XML/1998/namespace"
+                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
+     
+

+ Subsequently, qualified reference to any of the attributes or the + group defined below will have the desired effect, e.g. +

+
+          <type . . .>
+           . . .
+           <attributeGroup ref="xml:specialAttrs"/>
+     
+

+ will define a type which will schema-validate an instance element + with any of those attributes. +

+
+
+
+
+ + + +
+

Versioning policy for this schema document

+
+

+ In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + + http://www.w3.org/2009/01/xml.xsd. +

+

+ At the date of issue it can also be found at + + http://www.w3.org/2001/xml.xsd. +

+

+ The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML + Schema itself, or with the XML namespace itself. In other words, + if the XML Schema or XML namespaces change, the version of this + document at + http://www.w3.org/2001/xml.xsd + + will change accordingly; the version at + + http://www.w3.org/2009/01/xml.xsd + + will not change. +

+

+ Previous dated (and unchanging) versions of this schema + document are at: +

+ +
+
+
+
+ +
+ diff --git a/hapi-fhir-jpaserver-base/ca.uhn.fhir.jpa.entity.TermConcept/write.lock b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/test/java/.keep similarity index 100% rename from hapi-fhir-jpaserver-base/ca.uhn.fhir.jpa.entity.TermConcept/write.lock rename to hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/test/java/.keep diff --git a/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/test/resources/.keep_hapi-fhir-jpaserver-example b/hapi-fhir-jpaserver-examples/hapi-fhir-jpaserver-example-postgres/src/test/resources/.keep_hapi-fhir-jpaserver-example new file mode 100644 index 00000000000..e69de29bb2d diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/client/LoggingInterceptorTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/client/LoggingInterceptorTest.java index 0d14bf586e1..e18ad991253 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/client/LoggingInterceptorTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/client/LoggingInterceptorTest.java @@ -3,18 +3,17 @@ package ca.uhn.fhir.rest.client; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.mockito.Matchers.argThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.net.URL; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; +import org.junit.*; import org.mockito.ArgumentMatcher; import org.slf4j.LoggerFactory; @@ -29,18 +28,13 @@ import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.TestUtil; -import ch.qos.logback.classic.BasicConfigurator; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.joran.JoranConfigurator; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.LoggingEvent; -import ch.qos.logback.classic.util.LogbackMDCAdapter; import ch.qos.logback.core.Appender; -/** - * Created by dsotnikov on 2/25/2014. - */ public class LoggingInterceptorTest { private static FhirContext ourCtx = FhirContext.forDstu1(); diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/InterceptorTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/InterceptorTest.java index 6211ac045f7..7e7252b3e2c 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/InterceptorTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/InterceptorTest.java @@ -43,6 +43,7 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.TestUtil; @@ -87,6 +88,10 @@ public class InterceptorTest { order.verify(myInterceptor2, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class)); order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class)); + + order.verify(myInterceptor2, times(1)).processingCompletedNormally(any(ServletRequestDetails.class)); + order.verify(myInterceptor1, times(1)).processingCompletedNormally(any(ServletRequestDetails.class)); + verifyNoMoreInteractions(myInterceptor1); verifyNoMoreInteractions(myInterceptor2); } diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/util/XmlUtilTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/util/XmlUtilTest.java index 52c0828c7e1..f2ecfcf9353 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/util/XmlUtilTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/util/XmlUtilTest.java @@ -1,14 +1,45 @@ package ca.uhn.fhir.util; +import static org.junit.Assert.*; + import java.io.StringReader; import java.io.StringWriter; +import javax.xml.stream.FactoryConfigurationError; + +import org.junit.After; import org.junit.AfterClass; import org.junit.Test; public class XmlUtilTest { - + @Test + public void testCreateInputFactoryWithException() { + XmlUtil.setThrowExceptionForUnitTest(new Error("FOO ERROR")); + try { + XmlUtil.newInputFactory(); + fail(); + } catch (Exception e) { + assertEquals("Unable to initialize StAX - XML processing is disabled", e.getMessage()); + } + } + + @Test + public void testCreateOutputFactoryWithException() { + XmlUtil.setThrowExceptionForUnitTest(new Error("FOO ERROR")); + try { + XmlUtil.newOutputFactory(); + fail(); + } catch (Exception e) { + assertEquals("Unable to initialize StAX - XML processing is disabled", e.getMessage()); + } + } + + @After + public void after() { + XmlUtil.setThrowExceptionForUnitTest(null); + } + @Test public void testCreateReader() throws Exception { XmlUtil.createXmlReader(new StringReader("")); @@ -24,7 +55,6 @@ public class XmlUtilTest { XmlUtil.createXmlStreamWriter(new StringWriter()); } - @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java index 30f63f80874..ed43c0b8fe3 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/parser/XmlParserDstu2Test.java @@ -425,6 +425,38 @@ public class XmlParserDstu2Test { ourCtx = null; } + /** + * See #448 + */ + @Test + public void testParseWithMultipleProfiles() { + ourCtx = FhirContext.forDstu2(); + ourCtx.setDefaultTypeForProfile(CustomObservationDstu2.PROFILE, CustomObservationDstu2.class); + ourCtx.setDefaultTypeForProfile(CustomDiagnosticReportDstu2.PROFILE, CustomDiagnosticReportDstu2.class); + + //@formatter:off + String input = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + //@formatter:on + + IParser parser = ourCtx.newXmlParser(); + CustomDiagnosticReportDstu2 dr = (CustomDiagnosticReportDstu2) parser.parseResource(input); + assertEquals(DiagnosticReportStatusEnum.FINAL, dr.getStatusElement().getValueAsEnum()); + + List profiles = ResourceMetadataKeyEnum.PROFILES.get(dr); + assertEquals(3, profiles.size()); + + ourCtx = null; + } + + @Test public void testEncodeAndParseContainedNonCustomTypes() { ourCtx = FhirContext.forDstu2(); diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/InterceptorUserDataMapDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/InterceptorUserDataMapDstu2Test.java index 9e2b437d129..288b070bcdd 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/InterceptorUserDataMapDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/InterceptorUserDataMapDstu2Test.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.rest.server.interceptor; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -8,12 +9,7 @@ import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; import java.lang.reflect.Method; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.TimeUnit; import org.apache.commons.io.IOUtils; @@ -80,7 +76,7 @@ public class InterceptorUserDataMapDstu2Test { @Before public void beforePurgeMap() { myMap = null; - myMapCheckMethods= new HashSet(); + myMapCheckMethods= new LinkedHashSet(); } @@ -96,7 +92,7 @@ public class InterceptorUserDataMapDstu2Test { IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info(myMapCheckMethods.toString()); - assertThat(myMapCheckMethods, containsInAnyOrder("incomingRequestPreHandled", "handleException", "incomingRequestPostProcessed", "preProcessOutgoingException")); + assertThat(myMapCheckMethods, containsInAnyOrder("incomingRequestPostProcessed", "incomingRequestPreHandled", "preProcessOutgoingException", "handleException")); } @Test @@ -111,7 +107,7 @@ public class InterceptorUserDataMapDstu2Test { IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info(myMapCheckMethods.toString()); - assertThat(myMapCheckMethods, containsInAnyOrder("incomingRequestPreHandled", "incomingRequestPostProcessed", "outgoingResponse")); + assertThat(myMapCheckMethods, contains("incomingRequestPostProcessed", "incomingRequestPreHandled", "outgoingResponse", "processingCompletedNormally")); } protected void updateMapUsing(Map theUserData, Method theMethod) { diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptorDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptorDstu2Test.java index c92e004bccc..355ed125780 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptorDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/LoggingInterceptorDstu2Test.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.rest.server.interceptor; +import static org.hamcrest.Matchers.matchesPattern; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -59,9 +60,6 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.TestUtil; -/** - * Created by dsotnikov on 2/25/2014. - */ public class LoggingInterceptorDstu2Test { private static CloseableHttpClient ourClient; @@ -70,6 +68,7 @@ public class LoggingInterceptorDstu2Test { private static Server ourServer; private static RestfulServer servlet; private IServerInterceptor myInterceptor; + private static int ourDelayMs; private static Exception ourThrowException; @Before @@ -77,6 +76,7 @@ public class LoggingInterceptorDstu2Test { myInterceptor = mock(IServerInterceptor.class); servlet.setInterceptors(Collections.singletonList(myInterceptor)); ourThrowException = null; + ourDelayMs=0; } @Test @@ -98,9 +98,8 @@ public class LoggingInterceptorDstu2Test { IOUtils.closeQuietly(status.getEntity().getContent()); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(logger, times(2)).info(captor.capture()); - assertThat(captor.getAllValues().get(0), StringContains.containsString("read - Patient/EX")); - assertThat(captor.getAllValues().get(1), StringContains.containsString("ERROR - GET http://localhost:" + ourPort + "/Patient/EX")); + verify(logger, times(1)).info(captor.capture()); + assertThat(captor.getAllValues().get(0), StringContains.containsString("ERROR - GET http://localhost:" + ourPort + "/Patient/EX")); } @Test @@ -135,6 +134,10 @@ public class LoggingInterceptorDstu2Test { HttpResponse status = ourClient.execute(httpGet); IOUtils.closeQuietly(status.getEntity().getContent()); + // The server finishes the response and closes the connection, then runs the final interceptor so technically + // we could get here before the interceptor has fired + Thread.sleep(100); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(logger, times(1)).info(captor.capture()); assertEquals("extended-operation-instance - $everything - Patient/123", captor.getValue()); @@ -160,6 +163,31 @@ public class LoggingInterceptorDstu2Test { assertEquals("read - - Patient/1 - ", captor.getValue()); } + @Test + public void testProcessingTime() throws Exception { + ourDelayMs = 110; + + LoggingInterceptor interceptor = new LoggingInterceptor(); + interceptor.setMessageFormat("${processingTimeMillis}"); + servlet.setInterceptors(Collections.singletonList((IServerInterceptor) interceptor)); + + Logger logger = mock(Logger.class); + interceptor.setLogger(logger); + + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1"); + + HttpResponse status = ourClient.execute(httpGet); + IOUtils.closeQuietly(status.getEntity().getContent()); + + // The server finishes the response and closes the connection, then runs the final interceptor so technically + // we could get here before the interceptor has fired + Thread.sleep(100); + + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); + verify(logger, times(1)).info(captor.capture()); + assertThat(captor.getValue(), matchesPattern("[0-9]{3}")); + } + @Test public void testRequestBodyReadWithContentTypeHeader() throws Exception { @@ -201,6 +229,10 @@ public class LoggingInterceptorDstu2Test { HttpResponse status = ourClient.execute(httpPost); IOUtils.closeQuietly(status.getEntity().getContent()); + // The server finishes the response and closes the connection, then runs the final interceptor so technically + // we could get here before the interceptor has fired + Thread.sleep(100); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(logger, times(1)).info(captor.capture()); assertEquals("create - - Patient - ", captor.getValue()); @@ -211,6 +243,7 @@ public class LoggingInterceptorDstu2Test { LoggingInterceptor interceptor = new LoggingInterceptor(); interceptor.setMessageFormat("${operationType} - ${operationName} - ${idOrResourceName} - ${requestBodyFhir}"); + interceptor.setErrorMessageFormat("ERROR - ${operationType} - ${operationName} - ${idOrResourceName} - ${requestBodyFhir}"); servlet.setInterceptors(Collections.singletonList((IServerInterceptor) interceptor)); Logger logger = mock(Logger.class); @@ -228,9 +261,13 @@ public class LoggingInterceptorDstu2Test { HttpResponse status = ourClient.execute(httpPost); IOUtils.closeQuietly(status.getEntity().getContent()); + // The server finishes the response and closes the connection, then runs the final interceptor so technically + // we could get here before the interceptor has fired + Thread.sleep(100); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(logger, times(1)).info(captor.capture()); - assertEquals("create - - Patient - ", captor.getValue()); + assertEquals("ERROR - create - - Patient - ", captor.getValue()); } @Test @@ -265,7 +302,11 @@ public class LoggingInterceptorDstu2Test { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$everything"); HttpResponse status = ourClient.execute(httpGet); IOUtils.closeQuietly(status.getEntity().getContent()); - + + // The server finishes the response and closes the connection, then runs the final interceptor so technically + // we could get here before the interceptor has fired + Thread.sleep(100); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(logger, times(1)).info(captor.capture()); assertEquals("extended-operation-type - $everything - Patient", captor.getValue()); @@ -284,6 +325,10 @@ public class LoggingInterceptorDstu2Test { HttpResponse status = ourClient.execute(httpGet); IOUtils.closeQuietly(status.getEntity().getContent()); + // The server finishes the response and closes the connection, then runs the final interceptor so technically + // we could get here before the interceptor has fired + Thread.sleep(100); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(logger, times(1)).info(captor.capture()); assertThat(captor.getValue(), StringContains.containsString("read - Patient/1")); @@ -303,6 +348,10 @@ public class LoggingInterceptorDstu2Test { HttpResponse status = ourClient.execute(httpGet); IOUtils.closeQuietly(status.getEntity().getContent()); + // The server finishes the response and closes the connection, then runs the final interceptor so technically + // we could get here before the interceptor has fired + Thread.sleep(100); + ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); verify(logger, times(1)).info(captor.capture()); assertThat(captor.getValue(), StringContains.containsString("search-type - Patient - ?_id=1")); @@ -386,7 +435,11 @@ public class LoggingInterceptorDstu2Test { * @return The resource */ @Read() - public Patient getResourceById(@IdParam IdDt theId) { + public Patient getResourceById(@IdParam IdDt theId) throws InterruptedException { + if (ourDelayMs>0) { + Thread.sleep(ourDelayMs); + } + if (theId.getIdPart().equals("EX")) { throw new InvalidRequestException("FOO"); } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java index 793b014a7d7..ee9c2e3d509 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/ResponseHighlightingInterceptorTest.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.rest.server.interceptor; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.matchesPattern; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.stringContainsInOrder; import static org.junit.Assert.assertArrayEquals; @@ -14,6 +15,7 @@ import static org.mockito.Mockito.when; import java.io.PrintWriter; import java.io.StringWriter; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -87,12 +89,26 @@ public class ResponseHighlightingInterceptorTest { } + @Test + public void testForceResponseTime() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/json"); + + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); + assertThat(responseContent.replace('\n', ' ').replace('\r', ' '), matchesPattern(".*Response generated in [0-9]+ms.*")); + + } + @Test public void testGetInvalidResource() throws Exception { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Foobar/123"); httpGet.addHeader("Accept", "text/html"); CloseableHttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info("Resp: {}", responseContent); @@ -457,7 +473,7 @@ public class ResponseHighlightingInterceptorTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_query=searchWithWildcardRetVal&_summary=count"); httpGet.addHeader("Accept", "html"); CloseableHttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info("Resp: {}", responseContent); @@ -501,7 +517,7 @@ public class ResponseHighlightingInterceptorTest { httpGet.addHeader("Accept", Constants.CT_FHIR_JSON); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); @@ -515,7 +531,7 @@ public class ResponseHighlightingInterceptorTest { httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); @@ -527,7 +543,7 @@ public class ResponseHighlightingInterceptorTest { httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); @@ -539,7 +555,7 @@ public class ResponseHighlightingInterceptorTest { httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); @@ -552,7 +568,7 @@ public class ResponseHighlightingInterceptorTest { httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(Constants.CT_FHIR_JSON + ";charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); @@ -567,7 +583,7 @@ public class ResponseHighlightingInterceptorTest { httpGet.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1"); HttpResponse status = ourClient.execute(httpGet); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase()); diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/Dstu3BundleFactory.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/Dstu3BundleFactory.java index 0855fefb090..602436df57a 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/Dstu3BundleFactory.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/Dstu3BundleFactory.java @@ -21,13 +21,7 @@ package org.hl7.fhir.dstu3.hapi.rest.server; */ import static org.apache.commons.lang3.StringUtils.isNotBlank; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.UUID; +import java.util.*; import org.apache.commons.lang3.Validate; import org.hl7.fhir.dstu3.model.Bundle; @@ -35,25 +29,17 @@ import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; import org.hl7.fhir.dstu3.model.Bundle.BundleLinkComponent; import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb; import org.hl7.fhir.dstu3.model.Bundle.SearchEntryMode; -import org.hl7.fhir.instance.model.api.*; import org.hl7.fhir.dstu3.model.DomainResource; import org.hl7.fhir.dstu3.model.IdType; import org.hl7.fhir.dstu3.model.Resource; +import org.hl7.fhir.instance.model.api.*; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; -import ca.uhn.fhir.model.base.composite.BaseResourceReferenceDt; import ca.uhn.fhir.model.base.resource.BaseOperationOutcome; import ca.uhn.fhir.model.valueset.BundleTypeEnum; -import ca.uhn.fhir.rest.server.BundleInclusionRule; -import ca.uhn.fhir.rest.server.Constants; -import ca.uhn.fhir.rest.server.EncodingEnum; -import ca.uhn.fhir.rest.server.IBundleProvider; -import ca.uhn.fhir.rest.server.IPagingProvider; -import ca.uhn.fhir.rest.server.IRestfulServer; -import ca.uhn.fhir.rest.server.IVersionSpecificBundleFactory; -import ca.uhn.fhir.rest.server.RestfulServerUtils; +import ca.uhn.fhir.rest.server.*; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; import ca.uhn.fhir.util.ResourceReferenceInfo; @@ -135,6 +121,7 @@ public class Dstu3BundleFactory implements IVersionSpecificBundleFactory { String httpVerb = ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(next); if (httpVerb != null) { entry.getRequest().getMethodElement().setValueAsString(httpVerb); + entry.getRequest().getUrlElement().setValue(next.getId()); } } @@ -161,7 +148,7 @@ public class Dstu3BundleFactory implements IVersionSpecificBundleFactory { for (IBaseResource next : theResult) { if (next.getIdElement().isEmpty() == false) { - addedResourceIds.add((IdType) next.getIdElement()); + addedResourceIds.add(next.getIdElement()); } } @@ -221,11 +208,14 @@ public class Dstu3BundleFactory implements IVersionSpecificBundleFactory { BundleEntryComponent entry = myBundle.addEntry().setResource((Resource) next); Resource nextAsResource = (Resource)next; + IIdType id = populateBundleEntryFullUrl(next, entry); String httpVerb = ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.get(nextAsResource); if (httpVerb != null) { entry.getRequest().getMethodElement().setValueAsString(httpVerb); + if (id != null) { + entry.getRequest().setUrl(id.getValue()); + } } - populateBundleEntryFullUrl(next, entry); String searchMode = ResourceMetadataKeyEnum.ENTRY_SEARCH_MODE.get(nextAsResource); if (searchMode != null) { @@ -244,16 +234,19 @@ public class Dstu3BundleFactory implements IVersionSpecificBundleFactory { } - private void populateBundleEntryFullUrl(IBaseResource next, BundleEntryComponent entry) { + private IIdType populateBundleEntryFullUrl(IBaseResource next, BundleEntryComponent entry) { + IIdType idElement = null; if (next.getIdElement().hasBaseUrl()) { - entry.setFullUrl(next.getIdElement().toVersionless().getValue()); + idElement = next.getIdElement(); + entry.setFullUrl(idElement.toVersionless().getValue()); } else { if (isNotBlank(myBase) && next.getIdElement().hasIdPart()) { - IIdType id = next.getIdElement().toVersionless(); - id = id.withServerBase(myBase, myContext.getResourceDefinition(next).getName()); - entry.setFullUrl(id.getValue()); + idElement = next.getIdElement(); + idElement = idElement.withServerBase(myBase, myContext.getResourceDefinition(next).getName()); + entry.setFullUrl(idElement.toVersionless().getValue()); } } + return idElement; } @Override @@ -392,7 +385,7 @@ public class Dstu3BundleFactory implements IVersionSpecificBundleFactory { Resource next = (Resource) nextBaseRes; BundleEntryComponent nextEntry = myBundle.addEntry(); - nextEntry.setResource((Resource) next); + nextEntry.setResource(next); if (next.getIdElement().isEmpty()) { nextEntry.getRequest().setMethod(HTTPVerb.POST); } else { diff --git a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerConformanceProvider.java b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerConformanceProvider.java index 5c928d574f8..a160d117442 100644 --- a/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerConformanceProvider.java +++ b/hapi-fhir-structures-dstu3/src/main/java/org/hl7/fhir/dstu3/hapi/rest/server/ServerConformanceProvider.java @@ -223,8 +223,8 @@ public class ServerConformanceProvider implements IServerConformanceProviderFINAL VALUE", ((Patient) outcome.getResource()).getText().getDivAsString()); assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(0).getURI().toASCIIString()); - assertEquals(Constants.CT_FHIR_XML, capt.getAllValues().get(0).getFirstHeader("content-type").getValue().replaceAll(";.*", "")); - assertEquals("application/xml+fhir", capt.getAllValues().get(0).getFirstHeader("accept").getValue()); + assertEquals(Constants.CT_FHIR_XML_NEW, capt.getAllValues().get(0).getFirstHeader("content-type").getValue().replaceAll(";.*", "")); + assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY, capt.getAllValues().get(0).getFirstHeader("accept").getValue()); assertEquals("
A PATIENT
", extractBodyAsString(capt)); } @@ -86,8 +86,8 @@ public class ClientMimetypeDstu3Test { assertEquals("
FINAL VALUE
", ((Patient) outcome.getResource()).getText().getDivAsString()); assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(0).getURI().toASCIIString()); - assertEquals(Constants.CT_FHIR_XML, capt.getAllValues().get(0).getFirstHeader("content-type").getValue().replaceAll(";.*", "")); - assertEquals("application/xml+fhir", capt.getAllValues().get(0).getFirstHeader("accept").getValue()); + assertEquals(Constants.CT_FHIR_XML_NEW, capt.getAllValues().get(0).getFirstHeader("content-type").getValue().replaceAll(";.*", "")); + assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY, capt.getAllValues().get(0).getFirstHeader("accept").getValue()); assertEquals("
A PATIENT
", extractBodyAsString(capt)); } @@ -106,8 +106,8 @@ public class ClientMimetypeDstu3Test { assertEquals("
FINAL VALUE
", ((Patient) outcome.getResource()).getText().getDivAsString()); assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(0).getURI().toASCIIString()); - assertEquals(Constants.CT_FHIR_JSON, capt.getAllValues().get(0).getFirstHeader("content-type").getValue().replaceAll(";.*", "")); - assertEquals("application/json+fhir", capt.getAllValues().get(0).getFirstHeader("accept").getValue()); + assertEquals(Constants.CT_FHIR_JSON_NEW, capt.getAllValues().get(0).getFirstHeader("content-type").getValue().replaceAll(";.*", "")); + assertEquals(Constants.HEADER_ACCEPT_VALUE_JSON_NON_LEGACY, capt.getAllValues().get(0).getFirstHeader("accept").getValue()); assertEquals("{\"resourceType\":\"Patient\",\"text\":{\"div\":\"
A PATIENT
\"}}", extractBodyAsString(capt)); } @@ -126,8 +126,8 @@ public class ClientMimetypeDstu3Test { assertEquals("
FINAL VALUE
", ((Patient) outcome.getResource()).getText().getDivAsString()); assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(0).getURI().toASCIIString()); - assertEquals(Constants.CT_FHIR_JSON, capt.getAllValues().get(0).getFirstHeader("content-type").getValue().replaceAll(";.*", "")); - assertEquals("application/json+fhir", capt.getAllValues().get(0).getFirstHeader("accept").getValue()); + assertEquals(Constants.CT_FHIR_JSON_NEW, capt.getAllValues().get(0).getFirstHeader("content-type").getValue().replaceAll(";.*", "")); + assertEquals(Constants.HEADER_ACCEPT_VALUE_JSON_NON_LEGACY, capt.getAllValues().get(0).getFirstHeader("accept").getValue()); assertEquals("{\"resourceType\":\"Patient\",\"text\":{\"div\":\"
A PATIENT
\"}}", extractBodyAsString(capt)); } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java index 3b8bbc6c497..55618bae883 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu3Test.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.util.*; import org.apache.commons.io.IOUtils; @@ -26,6 +27,7 @@ import org.apache.http.ProtocolVersion; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +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.BasicStatusLine; @@ -62,6 +64,7 @@ import ca.uhn.fhir.rest.client.interceptor.CookieInterceptor; import ca.uhn.fhir.rest.client.interceptor.UserInfoInterceptor; import ca.uhn.fhir.rest.param.ParamPrefixEnum; import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException; import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException; import ca.uhn.fhir.util.TestUtil; @@ -201,8 +204,8 @@ public class GenericClientDstu3Test { assertEquals("http://example.com/fhir/Binary", capt.getAllValues().get(0).getURI().toASCIIString()); validateUserAgent(capt); - assertEquals("application/xml+fhir;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", "")); - assertEquals(Constants.CT_FHIR_XML, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue()); + assertEquals("application/fhir+xml;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", "")); + assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue()); Binary output = ourCtx.newXmlParser().parseResource(Binary.class, extractBodyAsString(capt)); assertEquals(Constants.CT_FHIR_JSON, output.getContentType()); @@ -293,8 +296,8 @@ public class GenericClientDstu3Test { assertEquals("http://example.com/fhir/Binary", capt.getAllValues().get(0).getURI().toASCIIString()); validateUserAgent(capt); - assertEquals("application/xml+fhir;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", "")); - assertEquals(Constants.CT_FHIR_XML, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue()); + assertEquals("application/fhir+xml;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", "")); + assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue()); assertArrayEquals(new byte[] { 0, 1, 2, 3, 4 }, ourCtx.newXmlParser().parseResource(Binary.class, extractBodyAsString(capt)).getContent()); } @@ -407,7 +410,7 @@ public class GenericClientDstu3Test { assertEquals(myAnswerCount, capt.getAllValues().size()); assertEquals("http://example.com/fhir/Patient", capt.getAllValues().get(0).getURI().toASCIIString()); - assertEquals(Constants.CT_FHIR_XML, capt.getAllValues().get(0).getFirstHeader("content-type").getValue().replaceAll(";.*", "")); + assertEquals(Constants.CT_FHIR_XML_NEW, capt.getAllValues().get(0).getFirstHeader("content-type").getValue().replaceAll(";.*", "")); assertEquals("http://foo.com/base/Patient/222/_history/3", capt.getAllValues().get(1).getURI().toASCIIString()); } @@ -735,6 +738,93 @@ public class GenericClientDstu3Test { } } + @Test + public void testPutDoesntForceAllIdsXml() throws Exception { + IParser p = ourCtx.newXmlParser(); + + Patient patient = new Patient(); + patient.setId("PATIENT1"); + patient.addName().addFamily("PATIENT1"); + + Bundle bundle = new Bundle(); + bundle.setId("BUNDLE1"); + bundle.addEntry().setResource(patient); + + final String encoded = p.encodeResourceToString(bundle); + assertEquals("", encoded); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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 ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(encoded), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + int idx = 0; + + //@formatter:off + client + .update() + .resource(bundle) + .prefer(PreferReturnEnum.REPRESENTATION) + .execute(); + + HttpPut httpRequest = (HttpPut) capt.getValue(); + assertEquals("http://example.com/fhir/Bundle/BUNDLE1", httpRequest.getURI().toASCIIString()); + + String requestString = IOUtils.toString(httpRequest.getEntity().getContent(), StandardCharsets.UTF_8); + assertEquals(encoded, requestString); + } + + @Test + public void testPutDoesntForceAllIdsJson() throws Exception { + IParser p = ourCtx.newJsonParser(); + + Patient patient = new Patient(); + patient.setId("PATIENT1"); + patient.addName().addFamily("PATIENT1"); + + Bundle bundle = new Bundle(); + bundle.setId("BUNDLE1"); + bundle.addEntry().setResource(patient); + + final String encoded = p.encodeResourceToString(bundle); + assertEquals("{\"resourceType\":\"Bundle\",\"id\":\"BUNDLE1\",\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"PATIENT1\",\"name\":[{\"family\":[\"PATIENT1\"]}]}}]}", encoded); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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_JSON_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(encoded), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + int idx = 0; + + //@formatter:off + client + .update() + .resource(bundle) + .prefer(PreferReturnEnum.REPRESENTATION) + .encodedJson() + .execute(); + + HttpPut httpRequest = (HttpPut) capt.getValue(); + assertEquals("http://example.com/fhir/Bundle/BUNDLE1", httpRequest.getURI().toASCIIString()); + + String requestString = IOUtils.toString(httpRequest.getEntity().getContent(), StandardCharsets.UTF_8); + assertEquals(encoded, requestString); + } + @Test public void testResponseHasContentTypeMissing() throws Exception { IParser p = ourCtx.newXmlParser(); @@ -1194,6 +1284,48 @@ public class GenericClientDstu3Test { } + @Test + public void testAcceptHeaderWithEncodingSpecified() throws Exception { + final String msg = "{\"resourceType\":\"Bundle\",\"id\":null,\"base\":\"http://localhost:57931/fhir/contextDev\",\"total\":1,\"link\":[{\"relation\":\"self\",\"url\":\"http://localhost:57931/fhir/contextDev/Patient?identifier=urn%3AMultiFhirVersionTest%7CtestSubmitPatient01&_format=json\"}],\"entry\":[{\"resource\":{\"resourceType\":\"Patient\",\"id\":\"1\",\"meta\":{\"versionId\":\"1\",\"lastUpdated\":\"2014-12-20T18:41:29.706-05:00\"},\"identifier\":[{\"system\":\"urn:MultiFhirVersionTest\",\"value\":\"testSubmitPatient01\"}]}}]}"; + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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_JSON + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).then(new Answer() { + @Override + public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")); + } + }); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + int idx = 0; + + //@formatter:off + client.setEncoding(EncodingEnum.JSON); + client.search() + .forResource("Device") + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Device?_format=json", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); + assertEquals("application/fhir+json;q=1.0, application/json+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue()); + idx++; + + //@formatter:off + client.setEncoding(EncodingEnum.XML); + client.search() + .forResource("Device") + .returnBundle(Bundle.class) + .execute(); + //@formatter:on + assertEquals("http://example.com/fhir/Device?_format=xml", UrlUtil.unescape(capt.getAllValues().get(idx).getURI().toString())); + assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", capt.getAllValues().get(idx).getFirstHeader(Constants.HEADER_ACCEPT).getValue()); + idx++; + + } + @Test public void testSearchForUnknownType() throws Exception { IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); @@ -1368,8 +1500,8 @@ public class GenericClientDstu3Test { assertEquals("http://example.com/fhir/Patient/111", capt.getAllValues().get(0).getURI().toASCIIString()); validateUserAgent(capt); - assertEquals("application/xml+fhir;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", "")); - assertEquals(Constants.CT_FHIR_XML, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue()); + assertEquals("application/fhir+xml;charset=utf-8", capt.getAllValues().get(0).getHeaders("Content-Type")[0].getValue().toLowerCase().replace(" ", "")); + assertEquals(Constants.HEADER_ACCEPT_VALUE_XML_NON_LEGACY, capt.getAllValues().get(0).getHeaders("Accept")[0].getValue()); String body = extractBodyAsString(capt); assertThat(body, containsString("")); } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/PatchClientDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/PatchClientDstu3Test.java new file mode 100644 index 00000000000..a383a3ce484 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/client/PatchClientDstu3Test.java @@ -0,0 +1,135 @@ +package ca.uhn.fhir.rest.client; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.Charset; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.ReaderInputStream; +import org.apache.http.HttpResponse; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicStatusLine; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.OperationOutcome; +import org.hl7.fhir.dstu3.model.Patient; +import org.junit.*; +import org.mockito.ArgumentCaptor; +import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Patch; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.PatchTypeEnum; +import ca.uhn.fhir.rest.client.api.IRestfulClient; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.util.TestUtil; + +public class PatchClientDstu3Test { + public interface IClientType extends IRestfulClient { + + @Patch(type=Patient.class) + MethodOutcome patch(@IdParam IdType theId, @ResourceParam String theBody, PatchTypeEnum thePatchType); + + } + + private static FhirContext ourCtx; + private HttpClient myHttpClient; + private HttpResponse myHttpResponse; + + @Before + public void before() { + myHttpClient = mock(HttpClient.class, new ReturnsDeepStubs()); + ourCtx.getRestfulClientFactory().setHttpClient(myHttpClient); + ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); + myHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); + } + + @Test + public void testJsonPatchAnnotation() throws Exception { + ArgumentCaptor capt = prepareResponse(); + + IClientType client = ourCtx.newRestfulClient(IClientType.class, "http://example.com/fhir"); + + Patient pt = new Patient(); + pt.getText().setDivAsString("A PATIENT"); + + MethodOutcome outcome = client.patch(new IdType("Patient/123"), "{}", PatchTypeEnum.JSON_PATCH); + + assertEquals("PATCH", capt.getAllValues().get(0).getMethod()); + assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(0).getURI().toASCIIString()); + assertEquals(Constants.CT_JSON_PATCH, capt.getAllValues().get(0).getFirstHeader("content-type").getValue().replaceAll(";.*", "")); + assertEquals("{}", extractBodyAsString(capt)); + assertEquals("
OK
", ((OperationOutcome) outcome.getOperationOutcome()).getText().getDivAsString()); + } + + @Test + @Ignore + public void testJsonPatchFluent() throws Exception { + ArgumentCaptor capt = prepareResponse(); + + IGenericClient client = ourCtx.newRestfulGenericClient("http://example.com/fhir"); + + Patient pt = new Patient(); + pt.getText().setDivAsString("A PATIENT"); + +// MethodOutcome outcome = client.patch().resource(""). + +// patch(new IdType("Patient/123"), "{}", PatchTypeEnum.JSON_PATCH); + +// assertEquals("PATCH", capt.getAllValues().get(0).getMethod()); +// assertEquals("http://example.com/fhir/Patient/123", capt.getAllValues().get(0).getURI().toASCIIString()); +// assertEquals(Constants.CT_JSON_PATCH, capt.getAllValues().get(0).getFirstHeader("content-type").getValue().replaceAll(";.*", "")); +// assertEquals("{}", extractBodyAsString(capt)); +// assertEquals("
OK
", ((OperationOutcome) outcome.getOperationOutcome()).getText().getDivAsString()); + } + + + private String extractBodyAsString(ArgumentCaptor capt) throws IOException { + String body = IOUtils.toString(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(0)).getEntity().getContent(), "UTF-8"); + return body; + } + + private ArgumentCaptor prepareResponse() throws IOException, ClientProtocolException { + final IParser p = ourCtx.newXmlParser(); + + final OperationOutcome resp1 = new OperationOutcome(); + resp1.getText().setDivAsString("OK"); + + ArgumentCaptor capt = ArgumentCaptor.forClass(HttpUriRequest.class); + when(myHttpClient.execute(capt.capture())).thenReturn(myHttpResponse); + 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_NEW + "; charset=UTF-8")); + when(myHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { + @Override + public ReaderInputStream answer(InvocationOnMock theInvocation) throws Throwable { + return new ReaderInputStream(new StringReader(p.encodeResourceToString(resp1)), Charset.forName("UTF-8")); + } + }); + return capt; + } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() { + ourCtx = FhirContext.forDstu3(); + } + +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/InterceptorDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/InterceptorDstu3Test.java index be1718d01d1..5f7eb480e65 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/InterceptorDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/InterceptorDstu3Test.java @@ -44,6 +44,7 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.TestUtil; @@ -112,6 +113,8 @@ public class InterceptorDstu3Test { order.verify(myInterceptor2, times(1)).incomingRequestPreHandled(any(RestOperationTypeEnum.class), any(ActionRequestDetails.class)); order.verify(myInterceptor2, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class)); order.verify(myInterceptor1, times(1)).outgoingResponse(any(RequestDetails.class), any(IResource.class)); + order.verify(myInterceptor2, times(1)).processingCompletedNormally(any(ServletRequestDetails.class)); + order.verify(myInterceptor1, times(1)).processingCompletedNormally(any(ServletRequestDetails.class)); verifyNoMoreInteractions(myInterceptor1); verifyNoMoreInteractions(myInterceptor2); diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/PatchDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/PatchDstu3Test.java new file mode 100644 index 00000000000..8628db313d2 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/PatchDstu3Test.java @@ -0,0 +1,191 @@ +package ca.uhn.fhir.rest.server; + +import static org.junit.Assert.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.io.IOUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPatch; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.OperationOutcome; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.IdParam; +import ca.uhn.fhir.rest.annotation.Patch; +import ca.uhn.fhir.rest.annotation.ResourceParam; +import ca.uhn.fhir.rest.api.PatchTypeEnum; +import ca.uhn.fhir.util.PortUtil; +import ca.uhn.fhir.util.TestUtil; + +public class PatchDstu3Test { + + private static CloseableHttpClient ourClient; + private static FhirContext ourCtx = FhirContext.forDstu3(); + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(PatchDstu3Test.class); + private static int ourPort; + private static Server ourServer; + private static String ourLastMethod; + private static PatchTypeEnum ourLastPatchType; + + @Before + public void before() { + ourLastMethod = null; + ourLastBody = null; + ourLastId = null; + } + + @Test + public void testPatchValidJson() throws Exception { + String requestContents = "[ { \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] } ]"; + HttpPatch httpPatch = new HttpPatch("http://localhost:" + ourPort + "/Patient/123"); + httpPatch.setEntity(new StringEntity(requestContents, ContentType.parse(Constants.CT_JSON_PATCH))); + CloseableHttpResponse status = ourClient.execute(httpPatch); + + try { + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("
OK
", responseContent); + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + assertEquals("patientPatch", ourLastMethod); + assertEquals("Patient/123", ourLastId.getValue()); + assertEquals(requestContents, ourLastBody); + assertEquals(PatchTypeEnum.JSON_PATCH, ourLastPatchType); + } + + @Test + public void testPatchValidXml() throws Exception { + String requestContents = ""; + HttpPatch httpPatch = new HttpPatch("http://localhost:" + ourPort + "/Patient/123"); + httpPatch.setEntity(new StringEntity(requestContents, ContentType.parse(Constants.CT_XML_PATCH))); + CloseableHttpResponse status = ourClient.execute(httpPatch); + + try { + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("
OK
", responseContent); + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + assertEquals("patientPatch", ourLastMethod); + assertEquals("Patient/123", ourLastId.getValue()); + assertEquals(requestContents, ourLastBody); + assertEquals(PatchTypeEnum.XML_PATCH, ourLastPatchType); + } + + @Test + public void testPatchValidJsonWithCharset() throws Exception { + String requestContents = "[ { \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] } ]"; + HttpPatch httpPatch = new HttpPatch("http://localhost:" + ourPort + "/Patient/123"); + httpPatch.setEntity(new StringEntity(requestContents, ContentType.parse(Constants.CT_JSON_PATCH + Constants.CHARSET_UTF8_CTSUFFIX))); + CloseableHttpResponse status = ourClient.execute(httpPatch); + + try { + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(200, status.getStatusLine().getStatusCode()); + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + assertEquals("patientPatch", ourLastMethod); + assertEquals("Patient/123", ourLastId.getValue()); + assertEquals(requestContents, ourLastBody); + } + + @Test + public void testPatchInvalidMimeType() throws Exception { + String requestContents = "[ { \"op\": \"add\", \"path\": \"/a/b/c\", \"value\": [ \"foo\", \"bar\" ] } ]"; + HttpPatch httpPatch = new HttpPatch("http://localhost:" + ourPort + "/Patient/123"); + httpPatch.setEntity(new StringEntity(requestContents, ContentType.parse("text/plain; charset=UTF-8"))); + CloseableHttpResponse status = ourClient.execute(httpPatch); + + try { + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info(responseContent); + assertEquals(400, status.getStatusLine().getStatusCode()); + assertEquals("", responseContent); + } finally { + IOUtils.closeQuietly(status.getEntity().getContent()); + } + + } + + @AfterClass + public static void afterClassClearContext() throws Exception { + ourServer.stop(); + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + @BeforeClass + public static void beforeClass() throws Exception { + ourPort = PortUtil.findFreePort(); + ourServer = new Server(ourPort); + + DummyPatientResourceProvider patientProvider = new DummyPatientResourceProvider(); + + ServletHandler proxyHandler = new ServletHandler(); + RestfulServer servlet = new RestfulServer(ourCtx); + servlet.setPagingProvider(new FifoMemoryPagingProvider(10)); + + servlet.setResourceProviders(patientProvider); + 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(); + + } + + private static String ourLastBody; + private static IdType ourLastId; + public static class DummyPatientResourceProvider implements IResourceProvider { + + + + @Override + public Class getResourceType() { + return Patient.class; + } + + //@formatter:off + @Patch + public OperationOutcome patientPatch(@IdParam IdType theId, PatchTypeEnum thePatchType, @ResourceParam String theBody) { + ourLastMethod = "patientPatch"; + ourLastBody = theBody; + ourLastId = theId; + ourLastPatchType = thePatchType; + OperationOutcome retVal = new OperationOutcome(); + retVal.getText().setDivAsString("
OK
"); + return retVal; + } + //@formatter:on + + } + +} diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ServerMimetypeDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ServerMimetypeDstu3Test.java index 0686cd9cd76..0e8656f9338 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ServerMimetypeDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/ServerMimetypeDstu3Test.java @@ -1,11 +1,13 @@ package ca.uhn.fhir.rest.server; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -25,10 +27,7 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; -import org.hl7.fhir.dstu3.model.DateType; -import org.hl7.fhir.dstu3.model.IdType; -import org.hl7.fhir.dstu3.model.OperationOutcome; -import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.instance.model.api.IBaseResource; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -53,6 +52,32 @@ public class ServerMimetypeDstu3Test { private static int ourPort; private static Server ourServer; + @Test + public void testConformanceMetadataUsesNewMimetypes() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/metadata"); + CloseableHttpResponse status = ourClient.execute(httpGet); + try { + String content = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); + Conformance conf = ourCtx.newXmlParser().parseResource(Conformance.class, content); + List strings = toStrings(conf.getFormat()); + assertThat(strings, contains(Constants.CT_FHIR_XML_NEW, Constants.CT_FHIR_JSON_NEW)); + } finally { + status.close(); + } + } + + + + private List toStrings(List theFormat) { + ArrayList retVal = new ArrayList<>(); + for (CodeType next : theFormat) { + retVal.add(next.asStringValue()); + } + return retVal; + } + + + @Test public void testCreateWithXmlLegacyNoAcceptHeader() throws Exception { Patient p = new Patient(); diff --git a/pom.xml b/pom.xml index cbd9d9f0dc5..5360e502cf1 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,17 @@ scm:git:git@github.com:jamesagnew/hapi-fhir.git + + + + false + + bintray-dnault-maven + bintray + http://dl.bintray.com/dnault/maven + + + @@ -241,6 +252,10 @@ Matt Clarke Orion Health + + FilipDomazet + Filip Domazet + @@ -300,6 +315,11 @@ logback-classic 1.1.7 + + com.github.dnault + xml-patch + 0.3.0 + com.google.guava guava @@ -387,6 +407,11 @@ reflow-velocity-tools 1.1.1 + + net.riotopsys + json_patch + 0.0.0 + net.sf.json-lib json-lib @@ -733,6 +758,13 @@ 1.6 1.6 + + 1.8 + 1.8 javac-with-errorprone true UTF-8 diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 542e5e7fb82..28da318b75a 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -7,6 +7,18 @@ + + Client, Server, and JPA server now support experimental support + for + + using the XML Patch and JSON Patch syntax as explored during the + September 2016 Baltimore Connectathon. See + this wiki page
]]> + for a description of the syntax. + ]]> + Thanks to Pater Girard for all of his help during the connectathon + in implementing this feature! + In server, when returning a list of resources, the server sometimes failed to add _include]]> resources to the response bundle if they were @@ -36,6 +48,60 @@ Times before 1970 with fractional milliseconds were parsed incorrectly. Thanks to GitHub user @CarthageKing for reporting! + + Prevent crash in parser when parsing resource + with multiple profile declarations when + default type for profile is used. Thanks to + Filip Domazet for the pull request! + + + STU3 servers were adding the old MimeType + strings to the + Conformance.format]]> + part of the generated server conformance + statement + + + When performing an update using the client on a resource that + contains other resources (e.g. Bundle update), all child resources in the + parent bundle were incorrectly given the ID of the parent. Thanks + to Filip Domazet for reporting! + + + STU clients now use an Accept header which + indicates support for both the old MimeTypes + (e.g. application/xml+fhir]]>) + and the new MimeTypes + (e.g. application/fhir+xml]]>) + + + JPA server now sends correct + HTTP 409 Version Conflict]]> + when a + DELETE fails because of constraint issues, instead of + HTTP 400 Invalid Request]]> + + + Server history operation did not populate the Bundle.entry.request.url + field, which is required in order for the bundle to pass validation. + Thanks to Richard Ettema for spotting this! + + + Add a new method to the server interceptor framework which will be + called after all other processing is complete (useful for performance + tracking). The server LoggingInterceptor has been switched to using this + method which means that log lines will be created when processing is finished, + instead of when it started. + + + STU3 clients were not sending the new mimetype values in the + Content-Type]]> header. Thanks to + Claude Nanjo for pointing this out! + + + JAX-RS server was not able to handle the new mime types defined + in STU3 + diff --git a/src/site/xdoc/doc_rest_operations.xml b/src/site/xdoc/doc_rest_operations.xml index 57062b5e134..392ce227348 100644 --- a/src/site/xdoc/doc_rest_operations.xml +++ b/src/site/xdoc/doc_rest_operations.xml @@ -218,6 +218,34 @@ + +

Contention Aware Updating

+ +

+ As of FHIR DSTU2, FHIR uses the ETag header to + provide "conention aware updating". Under this scheme, a client + may create a request that contains an ETag specifying the version, + and the server will fail if the given version is not the latest + version. +

+

+ Such a request is shown below. In the following example, the update will + only be applied if resource "Patient/123" is currently at version "3". + Otherwise, +

+
+ +

+ If a client performs a contention aware update, the ETag version will be + placed in the version part of the IdDt/IdType that is passed into the + method. For example: +

+ + + + +