From b183bed52fa8d75abddb5bb593495780519b03cb Mon Sep 17 00:00:00 2001 From: Volker Schmidt Date: Thu, 6 Sep 2018 19:33:34 +0800 Subject: [PATCH 01/11] Multiple values for one HTTP header were reduced to the last value. --- .../fhir/jaxrs/server/util/JaxRsResponse.java | 3 +- .../uhn/fhir/rest/server/RestfulResponse.java | 10 ++- .../servlet/ServletRestfulResponse.java | 15 ++++- .../fhir/rest/server/RestfulResponseTest.java | 35 ++++++++++ .../servlet/ServletRestfulResponseTest.java | 65 +++++++++++++++++++ 5 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulResponseTest.java create mode 100644 hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponseTest.java diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java index 182421e83c0..cb243896778 100644 --- a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java @@ -22,6 +22,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; * #L% */ import java.io.*; +import java.util.List; import java.util.Map.Entry; import javax.ws.rs.core.MediaType; @@ -104,7 +105,7 @@ public class JaxRsResponse extends RestfulResponse { private ResponseBuilder buildResponse(int statusCode) { ResponseBuilder response = Response.status(statusCode); - for (Entry header : getHeaders().entrySet()) { + for (Entry> header : getHeaders().entrySet()) { response.header(header.getKey(), header.getValue()); } return response; diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java index 2bd94a0047e..d56026d2046 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/RestfulResponse.java @@ -21,9 +21,7 @@ package ca.uhn.fhir.rest.server; */ import java.io.IOException; -import java.util.Date; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; +import java.util.*; import org.hl7.fhir.instance.model.api.*; @@ -35,7 +33,7 @@ public abstract class RestfulResponse implements IRest private IIdType myOperationResourceId; private IPrimitiveType myOperationResourceLastUpdated; - private ConcurrentHashMap theHeaders = new ConcurrentHashMap(); + private Map> theHeaders = new HashMap<>(); private T theRequestDetails; public RestfulResponse(T requestDetails) { @@ -44,14 +42,14 @@ public abstract class RestfulResponse implements IRest @Override public void addHeader(String headerKey, String headerValue) { - this.getHeaders().put(headerKey, headerValue); + this.getHeaders().computeIfAbsent(headerKey, k -> new ArrayList<>()).add(headerValue); } /** * Get the http headers * @return the headers */ - public ConcurrentHashMap getHeaders() { + public Map> getHeaders() { return theHeaders; } diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java index 3ffb3d9b9f1..969c0fa12b6 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponse.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; +import java.util.List; import java.util.Map.Entry; import java.util.zip.GZIPOutputStream; @@ -75,8 +76,18 @@ public class ServletRestfulResponse extends RestfulResponse header : getHeaders().entrySet()) { - theHttpResponse.setHeader(header.getKey(), header.getValue()); + for (Entry> header : getHeaders().entrySet()) { + final String key = header.getKey(); + boolean first = true; + for (String value : header.getValue()) { + // existing headers should be overridden + if (first) { + theHttpResponse.setHeader(key, value); + first = false; + } else { + theHttpResponse.addHeader(key, value); + } + } } } diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulResponseTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulResponseTest.java new file mode 100644 index 00000000000..009dbe3af5b --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/RestfulResponseTest.java @@ -0,0 +1,35 @@ +package ca.uhn.fhir.rest.server; + +import ca.uhn.fhir.rest.api.server.RequestDetails; +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.MockSettings; +import org.mockito.Mockito; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +/** + * Unit tests of {@link RestfulResponse}. + */ +public class RestfulResponseTest { + @Test + public void addMultipleHeaderValues() { + @SuppressWarnings("unchecked") + final RestfulResponse restfulResponse = + mock(RestfulResponse.class, withSettings() + .useConstructor((RequestDetails) null).defaultAnswer(CALLS_REAL_METHODS)); + + restfulResponse.addHeader("Authorization", "Basic"); + restfulResponse.addHeader("Authorization", "Bearer"); + restfulResponse.addHeader("Cache-Control", "no-cache, no-store"); + + assertEquals(2, restfulResponse.getHeaders().size()); + assertThat(restfulResponse.getHeaders().get("Authorization"), Matchers.contains("Basic", "Bearer")); + assertThat(restfulResponse.getHeaders().get("Cache-Control"), Matchers.contains("no-cache, no-store")); + } +} diff --git a/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponseTest.java b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponseTest.java new file mode 100644 index 00000000000..f56e475f7a2 --- /dev/null +++ b/hapi-fhir-server/src/test/java/ca/uhn/fhir/rest/server/servlet/ServletRestfulResponseTest.java @@ -0,0 +1,65 @@ +package ca.uhn.fhir.rest.server.servlet; + +import ca.uhn.fhir.rest.server.RestfulServer; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * Unit tests of {@link ServletRestfulResponse}. + */ +public class ServletRestfulResponseTest { + @Mock + private RestfulServer server; + + @Mock + private ServletOutputStream servletOutputStream; + + @Mock + private HttpServletResponse servletResponse; + + private ServletRequestDetails requestDetails; + + private ServletRestfulResponse response; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Before + public void init() throws IOException { + Mockito.when(servletResponse.getOutputStream()).thenReturn(servletOutputStream); + + requestDetails = new ServletRequestDetails(); + requestDetails.setServer(server); + requestDetails.setServletResponse(servletResponse); + response = new ServletRestfulResponse(requestDetails); + } + + @Test + public void addMultipleHeaderValues() throws IOException { + final ServletRestfulResponse response = new ServletRestfulResponse(requestDetails); + response.addHeader("Authorization", "Basic"); + response.addHeader("Authorization", "Bearer"); + response.addHeader("Cache-Control", "no-cache, no-store"); + + response.getResponseWriter(200, "Status", "text/plain", "UTF-8", false); + + final InOrder orderVerifier = Mockito.inOrder(servletResponse); + orderVerifier.verify(servletResponse).setHeader(eq("Authorization"), eq("Basic")); + orderVerifier.verify(servletResponse).addHeader(eq("Authorization"), eq("Bearer")); + verify(servletResponse).setHeader(eq("Cache-Control"), eq("no-cache, no-store")); + } +} From 35c440b7479d259b23b7edcf4b34696ec134d562 Mon Sep 17 00:00:00 2001 From: Volker Schmidt Date: Thu, 6 Sep 2018 20:59:49 +0800 Subject: [PATCH 02/11] Corrected multi-valued header value handling for JAX-RS. --- .../fhir/jaxrs/server/util/JaxRsResponse.java | 5 ++++- .../jaxrs/server/util/JaxRsResponseTest.java | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java index cb243896778..a79058e12d7 100644 --- a/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java +++ b/hapi-fhir-jaxrsserver-base/src/main/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponse.java @@ -106,7 +106,10 @@ public class JaxRsResponse extends RestfulResponse { private ResponseBuilder buildResponse(int statusCode) { ResponseBuilder response = Response.status(statusCode); for (Entry> header : getHeaders().entrySet()) { - response.header(header.getKey(), header.getValue()); + final String key = header.getKey(); + for (String value : header.getValue()) { + response.header(key, value); + } } return response; } diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponseTest.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponseTest.java index b9979da676b..c5b58275855 100644 --- a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponseTest.java +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/util/JaxRsResponseTest.java @@ -1,6 +1,7 @@ package ca.uhn.fhir.jaxrs.server.util; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.io.IOException; @@ -10,6 +11,7 @@ import java.util.Set; import javax.ws.rs.core.Response; +import org.hamcrest.Matchers; import org.hl7.fhir.instance.model.api.IBaseBinary; import org.junit.Before; import org.junit.Test; @@ -108,10 +110,24 @@ public class JaxRsResponseTest { assertEquals("application/xml+fhir; charset=UTF-8", result.getHeaderString(Constants.HEADER_CONTENT_TYPE)); } + @Test + public void addMultipleHeaderValues() throws IOException { + response.addHeader("Authorization", "Basic"); + response.addHeader("Authorization", "Bearer"); + response.addHeader("Cache-Control", "no-cache, no-store"); + + final IBaseBinary binary = new Binary(); + binary.setContentType("abc"); + binary.setContent(new byte[] { 1 }); + final Response result = (Response) RestfulServerUtils.streamResponseAsResource(request.getServer(), binary, theSummaryMode, 200, false, false, this.request); + + assertThat(result.getHeaders().get("Authorization"), Matchers.contains("Basic", "Bearer")); + assertThat(result.getHeaders().get("Cache-Control"), Matchers.contains("no-cache, no-store")); + } + private Patient createPatient() { Patient theResource = new Patient(); theResource.setId(new IdDt(15L)); return theResource; } - } From 729bbe04d0fd1bcce2981d8b88f66ef1ad42071e Mon Sep 17 00:00:00 2001 From: Ruth Alkema Date: Fri, 2 Mar 2018 14:41:24 +0100 Subject: [PATCH 03/11] Allow slotting in own IMessageResolver This is useful in case we want to define our own way of translating the codes in the thymeleaf templates. --- .../BaseThymeleafNarrativeGenerator.java | 13 +++++ ...tThymeleafNarrativeGeneratorDstu3Test.java | 53 +++++++++++++++++++ .../src/test/resources/TestPatient.html | 4 ++ .../test/resources/testnarrative.properties | 2 + 4 files changed, 72 insertions(+) create mode 100644 hapi-fhir-structures-dstu3/src/test/resources/TestPatient.html create mode 100644 hapi-fhir-structures-dstu3/src/test/resources/testnarrative.properties diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java index b9769cb99f0..9ab0c192c8e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java @@ -34,6 +34,7 @@ import org.thymeleaf.cache.ICacheEntryValidity; import org.thymeleaf.context.Context; import org.thymeleaf.context.ITemplateContext; import org.thymeleaf.engine.AttributeName; +import org.thymeleaf.messageresolver.IMessageResolver; import org.thymeleaf.model.IProcessableElementTag; import org.thymeleaf.processor.IProcessor; import org.thymeleaf.processor.element.AbstractAttributeTagProcessor; @@ -65,6 +66,8 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener private HashMap myNameToNarrativeTemplate; private TemplateEngine myProfileTemplateEngine; + private IMessageResolver resolver; + /** * Constructor */ @@ -166,11 +169,21 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener }; myProfileTemplateEngine.setDialect(dialect); + if (this.resolver != null) { + myProfileTemplateEngine.setMessageResolver(this.resolver); + } } myInitialized = true; } + public void setMessageResolver(IMessageResolver resolver) { + this.resolver = resolver; + if (myProfileTemplateEngine != null && resolver != null) { + myProfileTemplateEngine.setMessageResolver(resolver); + } + } + /** * If set to true (which is the default), most whitespace will be trimmed from the generated narrative * before it is returned. diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java index 62b9f81d527..aa427a35a67 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorDstu3Test.java @@ -5,7 +5,12 @@ import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import org.apache.commons.collections.Transformer; +import org.apache.commons.collections.map.LazyMap; import org.hamcrest.core.StringContains; import org.hl7.fhir.dstu3.model.CodeableConcept; import org.hl7.fhir.dstu3.model.Coding; @@ -28,6 +33,8 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import org.thymeleaf.messageresolver.StandardMessageResolver; +import org.thymeleaf.templateresource.ITemplateResource; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.DataFormatException; @@ -77,6 +84,52 @@ public class DefaultThymeleafNarrativeGeneratorDstu3Test { } + @Test + public void testTranslations() throws DataFormatException { + CustomThymeleafNarrativeGenerator customGen = new CustomThymeleafNarrativeGenerator("classpath:/testnarrative.properties"); + customGen.setIgnoreFailures(false); + customGen.setIgnoreMissingTemplates(false); + + FhirContext ctx = FhirContext.forDstu3(); + ctx.setNarrativeGenerator(customGen); + + Patient value = new Patient(); + + value.addIdentifier().setSystem("urn:names").setValue("123456"); + value.addName().setFamily("blow").addGiven("joe").addGiven((String) null).addGiven("john"); + //@formatter:off + value.addAddress() + .addLine("123 Fake Street").addLine("Unit 1") + .setCity("Toronto").setState("ON").setCountry("Canada"); + //@formatter:on + + value.setBirthDate(new Date()); + + Transformer transformer = new Transformer() { + + @Override + public Object transform(Object input) { + return "UNTRANSLATED:" + input; + }}; + + Map translations = new HashMap<>(); + translations.put("some_text", "Some beautiful proze"); + + customGen.setMessageResolver(new StandardMessageResolver() { + @Override + protected Map resolveMessagesForTemplate(String template, + ITemplateResource templateResource, Locale locale) { + return LazyMap.decorate(translations, transformer); + } + }); + + Narrative narrative = new Narrative(); + customGen.generateNarrative(ctx, value, narrative); + String output = narrative.getDiv().getValueAsString(); + ourLog.info(output); + assertThat(output, StringContains.containsString("Some beautiful proze")); + assertThat(output, StringContains.containsString("UNTRANSLATED:other_text")); + } @Test public void testGenerateDiagnosticReport() throws DataFormatException { diff --git a/hapi-fhir-structures-dstu3/src/test/resources/TestPatient.html b/hapi-fhir-structures-dstu3/src/test/resources/TestPatient.html new file mode 100644 index 00000000000..88d60488a3e --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/TestPatient.html @@ -0,0 +1,4 @@ +
+

Some Text

+

Some Text

+
diff --git a/hapi-fhir-structures-dstu3/src/test/resources/testnarrative.properties b/hapi-fhir-structures-dstu3/src/test/resources/testnarrative.properties new file mode 100644 index 00000000000..22f3b3c51b0 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/resources/testnarrative.properties @@ -0,0 +1,2 @@ +patient.class=org.hl7.fhir.dstu3.model.Patient +patient.narrative=classpath:/TestPatient.html From cca49425ae291d5bf5d04337b8e27f784c66157c Mon Sep 17 00:00:00 2001 From: James Agnew Date: Thu, 8 Nov 2018 08:56:51 -0500 Subject: [PATCH 04/11] Some work on collection processing --- .../fhir/jpa/dao/TransactionProcessor.java | 91 ++++++++++++------ ...ansactionProcessorVersionAdapterDstu3.java | 10 ++ .../TransactionProcessorVersionAdapterR4.java | 20 +++- .../jpa/dao/dstu3/FhirSystemDaoDstu3Test.java | 19 ++++ .../resources/dstu3/Reilly_Libby_73.json.gz | Bin 0 -> 29111 bytes 5 files changed, 107 insertions(+), 33 deletions(-) create mode 100644 hapi-fhir-jpaserver-base/src/test/resources/dstu3/Reilly_Libby_73.json.gz diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java index bec7998d5cc..dc1fe622d3c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/TransactionProcessor.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao; * 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. @@ -43,10 +43,7 @@ import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.method.BaseMethodBinding; import ca.uhn.fhir.rest.server.method.BaseResourceReturningMethodBinding; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import ca.uhn.fhir.util.FhirTerser; -import ca.uhn.fhir.util.ResourceReferenceInfo; -import ca.uhn.fhir.util.StopWatch; -import ca.uhn.fhir.util.UrlUtil; +import ca.uhn.fhir.util.*; import com.google.common.collect.ArrayListMultimap; import org.apache.commons.lang3.Validate; import org.apache.http.NameValuePair; @@ -86,17 +83,6 @@ public class TransactionProcessor { @Autowired private DaoRegistry myDaoRegistry; - public static boolean isPlaceholder(IIdType theId) { - if (theId != null && theId.getValue() != null) { - return theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:"); - } - return false; - } - - private static String toStatusString(int theStatusCode) { - return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); - } - private void populateEntryWithOperationOutcome(BaseServerResponseException caughtEx, BUNDLEENTRY nextEntry) { myVersionAdapter.populateEntryWithOperationOutcome(caughtEx, nextEntry); } @@ -164,7 +150,6 @@ public class TransactionProcessor { return defaultString(theId.getValue()).startsWith(URN_PREFIX); } - public void setDao(BaseHapiFhirDao theDao) { myDao = theDao; } @@ -188,6 +173,40 @@ public class TransactionProcessor { } } + public BUNDLE collection(final RequestDetails theRequestDetails, BUNDLE theRequest) { + String transactionType = myVersionAdapter.getBundleType(theRequest); + + if (!org.hl7.fhir.r4.model.Bundle.BundleType.COLLECTION.toCode().equals(transactionType)) { + throw new InvalidRequestException("Can not process collection Bundle of type: " + transactionType); + } + + ourLog.info("Beginning storing collection with {} resources", myVersionAdapter.getEntries(theRequest).size()); + long start = System.currentTimeMillis(); + + TransactionTemplate txTemplate = new TransactionTemplate(myTxManager); + txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + + BUNDLE resp = myVersionAdapter.createBundle(org.hl7.fhir.r4.model.Bundle.BundleType.BATCHRESPONSE.toCode()); + + List resources = new ArrayList<>(); + for (final BUNDLEENTRY nextRequestEntry : myVersionAdapter.getEntries(theRequest)) { + IBaseResource resource = myVersionAdapter.getResource(nextRequestEntry); + resources.add(resource); + } + + BUNDLE transactionBundle = myVersionAdapter.createBundle("transaction"); + for (IBaseResource next : resources) { + BUNDLEENTRY entry = myVersionAdapter.addEntry(transactionBundle); + myVersionAdapter.setResource(entry, next); + myVersionAdapter.setRequestVerb(entry, "PUT"); + myVersionAdapter.setRequestUrl(entry, next.getIdElement().toUnqualifiedVersionless().getValue()); + } + + transaction(theRequestDetails, transactionBundle); + + return resp; + } + private BUNDLE batch(final RequestDetails theRequestDetails, BUNDLE theRequest) { ourLog.info("Beginning batch with {} resources", myVersionAdapter.getEntries(theRequest).size()); long start = System.currentTimeMillis(); @@ -255,6 +274,7 @@ public class TransactionProcessor { validateDependencies(); String transactionType = myVersionAdapter.getBundleType(theRequest); + if (org.hl7.fhir.r4.model.Bundle.BundleType.BATCH.toCode().equals(transactionType)) { return batch(theRequestDetails, theRequest); } @@ -846,18 +866,10 @@ public class TransactionProcessor { String getEntryRequestIfNoneMatch(BUNDLEENTRY theEntry); void setResponseOutcome(BUNDLEENTRY theEntry, IBaseOperationOutcome theOperationOutcome); - } - private static class BaseServerResponseExceptionHolder { - private BaseServerResponseException myException; + void setRequestVerb(BUNDLEENTRY theEntry, String theVerb); - public BaseServerResponseException getException() { - return myException; - } - - public void setException(BaseServerResponseException myException) { - this.myException = myException; - } + void setRequestUrl(BUNDLEENTRY theEntry, String theUrl); } /** @@ -963,4 +975,27 @@ public class TransactionProcessor { } + private static class BaseServerResponseExceptionHolder { + private BaseServerResponseException myException; + + public BaseServerResponseException getException() { + return myException; + } + + public void setException(BaseServerResponseException myException) { + this.myException = myException; + } + } + + public static boolean isPlaceholder(IIdType theId) { + if (theId != null && theId.getValue() != null) { + return theId.getValue().startsWith("urn:oid:") || theId.getValue().startsWith("urn:uuid:"); + } + return false; + } + + private static String toStatusString(int theStatusCode) { + return Integer.toString(theStatusCode) + " " + defaultString(Constants.HTTP_STATUS_NAMES.get(theStatusCode)); + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/TransactionProcessorVersionAdapterDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/TransactionProcessorVersionAdapterDstu3.java index ddb198db705..0a98816c6c0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/TransactionProcessorVersionAdapterDstu3.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/TransactionProcessorVersionAdapterDstu3.java @@ -150,4 +150,14 @@ public class TransactionProcessorVersionAdapterDstu3 implements TransactionProce theEntry.getResponse().setOutcome((Resource) theOperationOutcome); } + @Override + public void setRequestVerb(Bundle.BundleEntryComponent theEntry, String theVerb) { + theEntry.getRequest().setMethod(Bundle.HTTPVerb.fromCode(theVerb)); + } + + @Override + public void setRequestUrl(Bundle.BundleEntryComponent theEntry, String theUrl) { + theEntry.getRequest().setUrl(theUrl); + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/TransactionProcessorVersionAdapterR4.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/TransactionProcessorVersionAdapterR4.java index db5be3e65c0..a9b510b9876 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/TransactionProcessorVersionAdapterR4.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/r4/TransactionProcessorVersionAdapterR4.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao.r4; * 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. @@ -23,12 +23,12 @@ package ca.uhn.fhir.jpa.dao.r4; import ca.uhn.fhir.jpa.dao.TransactionProcessor; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.OperationOutcome; -import org.hl7.fhir.r4.model.Resource; import org.hl7.fhir.exceptions.FHIRException; import org.hl7.fhir.instance.model.api.IBaseOperationOutcome; import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Resource; import java.util.Date; import java.util.List; @@ -150,4 +150,14 @@ public class TransactionProcessorVersionAdapterR4 implements TransactionProcesso theEntry.getResponse().setOutcome((Resource) theOperationOutcome); } + @Override + public void setRequestVerb(Bundle.BundleEntryComponent theEntry, String theVerb) { + theEntry.getRequest().setMethod(Bundle.HTTPVerb.fromCode(theVerb)); + } + + @Override + public void setRequestUrl(Bundle.BundleEntryComponent theEntry, String theUrl) { + theEntry.getRequest().setUrl(theUrl); + } + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java index effc8d746bd..68dc1ec6bd6 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java @@ -2,12 +2,14 @@ package ca.uhn.fhir.jpa.dao.dstu3; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.DaoConfig; +import ca.uhn.fhir.jpa.dao.GZipUtil; import ca.uhn.fhir.jpa.dao.SearchParameterMap; import ca.uhn.fhir.jpa.entity.ResourceTag; import ca.uhn.fhir.jpa.entity.TagTypeEnum; import ca.uhn.fhir.jpa.provider.SystemProviderDstu2Test; import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum; import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.parser.LenientErrorHandler; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.server.IBundleProvider; @@ -52,6 +54,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { public void after() { myDaoConfig.setAllowInlineMatchUrlReferences(false); myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete()); + myDaoConfig.setProcessCollectionsAsBatch(new DaoConfig().isProcessCollectionsAsBatch()); } @Before @@ -322,6 +325,22 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { } + + @Test + public void testProcessCollectionAsBatch() throws IOException { + myDaoConfig.setProcessCollectionsAsBatch(true); + + byte[] inputBytes = IOUtils.toByteArray(getClass().getResourceAsStream("/dstu3/Reilly_Libby_73.json.gz")); + String input = GZipUtil.decompress(inputBytes); + Bundle bundle = myFhirCtx.newJsonParser().setParserErrorHandler(new LenientErrorHandler()).parseResource(Bundle.class, input); + ourLog.info("Bundle has {} resources", bundle); + + Bundle output = mySystemDao.transaction(mySrd, bundle); + ourLog.info(myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(output)); + } + + + /** * See #410 */ diff --git a/hapi-fhir-jpaserver-base/src/test/resources/dstu3/Reilly_Libby_73.json.gz b/hapi-fhir-jpaserver-base/src/test/resources/dstu3/Reilly_Libby_73.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..17ab03b36accf62a51460a2e2b62588d661f380d GIT binary patch literal 29111 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

~ZCUREYXaL#7zjkK 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 z%0A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 literal 0 HcmV?d00001 From 9fee827398b8555225d61390b52a3340aea4116f Mon Sep 17 00:00:00 2001 From: James Agnew Date: Thu, 8 Nov 2018 13:24:24 -0500 Subject: [PATCH 05/11] Credit for #1103 --- pom.xml | 1 + src/changes/changes.xml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/pom.xml b/pom.xml index 1802c041406..19fadb07f72 100644 --- a/pom.xml +++ b/pom.xml @@ -433,6 +433,7 @@ RuthAlk + Ruth Alkema Tastelezz diff --git a/src/changes/changes.xml b/src/changes/changes.xml index da97486a69e..a71cd673aff 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -164,6 +164,10 @@ now possible to disallow this action, to only allow alphanumeric IDs (the default and only option previously) or allow any IDs including alphanumeric. + + It is now possible to use your own IMessageResolver instance in the narrative + generator. Thanks to Ruth Alkema for the pull request! + From 7e2e53a354db025bdded5a5507dcac479aea17e1 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Thu, 8 Nov 2018 13:33:25 -0500 Subject: [PATCH 06/11] Credit for #1071 --- pom.xml | 4 ++++ src/changes/changes.xml | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 19fadb07f72..7b80c7698a6 100644 --- a/pom.xml +++ b/pom.xml @@ -474,6 +474,10 @@ jbalbien + + volsch + Volker Schmidt + diff --git a/src/changes/changes.xml b/src/changes/changes.xml index a71cd673aff..8d7249421d2 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -164,10 +164,14 @@ now possible to disallow this action, to only allow alphanumeric IDs (the default and only option previously) or allow any IDs including alphanumeric. - + It is now possible to use your own IMessageResolver instance in the narrative generator. Thanks to Ruth Alkema for the pull request! + + When restful reponses tried to return multiple instances of the same response header, + some instances were discarded. Thanks to Volker Schmidt for the pull request! + " From 3ae5f9a3b7da2c28dcedb875502d2e25bd911084 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Thu, 8 Nov 2018 14:04:21 -0500 Subject: [PATCH 07/11] Build tweak --- .../ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java index 68dc1ec6bd6..280bb743ffa 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3Test.java @@ -54,7 +54,6 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { public void after() { myDaoConfig.setAllowInlineMatchUrlReferences(false); myDaoConfig.setAllowMultipleDelete(new DaoConfig().isAllowMultipleDelete()); - myDaoConfig.setProcessCollectionsAsBatch(new DaoConfig().isProcessCollectionsAsBatch()); } @Before @@ -327,9 +326,8 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { @Test + @Ignore public void testProcessCollectionAsBatch() throws IOException { - myDaoConfig.setProcessCollectionsAsBatch(true); - byte[] inputBytes = IOUtils.toByteArray(getClass().getResourceAsStream("/dstu3/Reilly_Libby_73.json.gz")); String input = GZipUtil.decompress(inputBytes); Bundle bundle = myFhirCtx.newJsonParser().setParserErrorHandler(new LenientErrorHandler()).parseResource(Bundle.class, input); @@ -340,7 +338,6 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { } - /** * See #410 */ @@ -3059,7 +3056,7 @@ public class FhirSystemDaoDstu3Test extends BaseJpaDstu3SystemTest { @Test public void testTransactionWithReplacement() { - byte[] bytes = new byte[] {0, 1, 2, 3, 4}; + byte[] bytes = new byte[]{0, 1, 2, 3, 4}; Binary binary = new Binary(); binary.setId(IdType.newRandomUuid()); From 82ec721c99cfc25f98835e4fb9b2aa2746175406 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Thu, 8 Nov 2018 15:27:35 -0500 Subject: [PATCH 08/11] Allow client defined parameter style for _format param --- .../rest/api/RequestFormatParamStyleEnum.java | 14 ++ .../fhir/rest/client/api/IRestfulClient.java | 56 +++--- .../uhn/fhir/rest/client/impl/BaseClient.java | 181 ++++++++---------- .../fhir/rest/client/ClientHeadersR4Test.java | 160 +++++++++++----- src/changes/changes.xml | 7 +- 5 files changed, 241 insertions(+), 177 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RequestFormatParamStyleEnum.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RequestFormatParamStyleEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RequestFormatParamStyleEnum.java new file mode 100644 index 00000000000..53d05ea2e31 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/RequestFormatParamStyleEnum.java @@ -0,0 +1,14 @@ +package ca.uhn.fhir.rest.api; + +public enum RequestFormatParamStyleEnum { + /** + * Do not include a _format parameter on requests + */ + NONE, + + /** + * "xml" or "json" + */ + SHORT + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClient.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClient.java index 36fd4fea5b0..f3f961edd64 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClient.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/client/api/IRestfulClient.java @@ -1,5 +1,11 @@ package ca.uhn.fhir.rest.client.api; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.EncodingEnum; +import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum; +import ca.uhn.fhir.rest.api.SummaryEnum; +import org.hl7.fhir.instance.model.api.IBaseResource; + import java.util.List; /* @@ -11,9 +17,9 @@ import java.util.List; * 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. @@ -22,23 +28,15 @@ import java.util.List; * #L% */ -import org.hl7.fhir.instance.model.api.IBaseResource; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.api.SummaryEnum; - public interface IRestfulClient { /** * Retrieve the contents at the given URL and parse them as a resource. This * method could be used as a low level implementation of a read/vread/search * operation. - * - * @param theResourceType - * The resource type to parse - * @param theUrl - * The URL to load + * + * @param theResourceType The resource type to parse + * @param theUrl The URL to load * @return The parsed resource */ T fetchResourceFromUrl(Class theResourceType, String theUrl); @@ -49,6 +47,17 @@ public interface IRestfulClient { */ EncodingEnum getEncoding(); + /** + * Specifies that the client should use the given encoding to do its + * queries. This means that the client will append the "_format" param + * to GET methods (read/search/etc), and will add an appropriate header for + * write methods. + * + * @param theEncoding The encoding to use in the request, or null not specify + * an encoding (which generally implies the use of XML). The default is null. + */ + void setEncoding(EncodingEnum theEncoding); + /** * Returns the FHIR context associated with this client */ @@ -76,25 +85,12 @@ public interface IRestfulClient { */ void registerInterceptor(IClientInterceptor theInterceptor); - /** - * Specifies that the client should use the given encoding to do its - * queries. This means that the client will append the "_format" param - * to GET methods (read/search/etc), and will add an appropriate header for - * write methods. - * - * @param theEncoding - * The encoding to use in the request, or null not specify - * an encoding (which generally implies the use of XML). The default is null. - */ - void setEncoding(EncodingEnum theEncoding); - /** * Specifies that the client should request that the server respond with "pretty printing" * enabled. Note that this is a non-standard parameter, not all servers will * support it. - * - * @param thePrettyPrint - * The pretty print flag to use in the request (default is false) + * + * @param thePrettyPrint The pretty print flag to use in the request (default is false) */ void setPrettyPrint(Boolean thePrettyPrint); @@ -109,4 +105,8 @@ public interface IRestfulClient { */ void unregisterInterceptor(IClientInterceptor theInterceptor); + /** + * Configures what style of _format parameter should be used in requests + */ + void setFormatParamStyle(RequestFormatParamStyleEnum theRequestFormatParamStyle); } diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/BaseClient.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/BaseClient.java index 0b447575917..3b1f404075a 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/BaseClient.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/BaseClient.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.client.impl; * 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. @@ -20,49 +20,11 @@ package ca.uhn.fhir.rest.client.impl; * #L% */ -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.stream.Collectors; - -import ca.uhn.fhir.rest.api.CacheControlDirective; -import ca.uhn.fhir.util.XmlDetectionUtil; -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 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.Constants; -import ca.uhn.fhir.rest.api.EncodingEnum; -import ca.uhn.fhir.rest.api.SummaryEnum; -import ca.uhn.fhir.rest.client.api.IClientInterceptor; -import ca.uhn.fhir.rest.client.api.IHttpClient; -import ca.uhn.fhir.rest.client.api.IHttpRequest; -import ca.uhn.fhir.rest.client.api.IHttpResponse; -import ca.uhn.fhir.rest.client.api.IRestfulClient; -import ca.uhn.fhir.rest.client.api.IRestfulClientFactory; -import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; +import ca.uhn.fhir.rest.api.*; +import ca.uhn.fhir.rest.client.api.*; import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; import ca.uhn.fhir.rest.client.exceptions.InvalidResponseException; import ca.uhn.fhir.rest.client.exceptions.NonFhirResponseException; @@ -72,7 +34,19 @@ import ca.uhn.fhir.rest.client.method.IClientResponseHandlerHandlesBinary; import ca.uhn.fhir.rest.client.method.MethodUtil; import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; import ca.uhn.fhir.util.OperationOutcomeUtil; -import ca.uhn.fhir.util.XmlUtil; +import ca.uhn.fhir.util.XmlDetectionUtil; +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.*; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.util.*; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; public abstract class BaseClient implements IRestfulClient { @@ -86,16 +60,17 @@ public abstract class BaseClient implements IRestfulClient { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(BaseClient.class); private final IHttpClient myClient; + private final RestfulClientFactory myFactory; + private final String myUrlBase; private boolean myDontValidateConformance; private EncodingEnum myEncoding = null; // default unspecified (will be XML) - private final RestfulClientFactory myFactory; private List myInterceptors = new ArrayList(); private boolean myKeepResponses = false; private IHttpResponse myLastResponse; private String myLastResponseBody; private Boolean myPrettyPrint = false; private SummaryEnum mySummary; - private final String myUrlBase; + private RequestFormatParamStyleEnum myRequestFormatParamStyle = RequestFormatParamStyleEnum.SHORT; BaseClient(IHttpClient theClient, String theUrlBase, RestfulClientFactory theFactory) { super(); @@ -121,10 +96,12 @@ public abstract class BaseClient implements IRestfulClient { protected Map> createExtraParams() { HashMap> retVal = new LinkedHashMap>(); - if (getEncoding() == EncodingEnum.XML) { - retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); - } else if (getEncoding() == EncodingEnum.JSON) { - retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); + if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) { + if (getEncoding() == EncodingEnum.XML) { + retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); + } else if (getEncoding() == EncodingEnum.JSON) { + retVal.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); + } } if (isPrettyPrint()) { @@ -150,6 +127,17 @@ public abstract class BaseClient implements IRestfulClient { return myEncoding; } + /** + * Sets the encoding that will be used on requests. Default is null, which means the client will not + * explicitly request an encoding. (This is perfectly acceptable behaviour according to the FHIR specification. In + * this case, the server will choose which encoding to return, and the client can handle either XML or JSON) + */ + @Override + public void setEncoding(EncodingEnum theEncoding) { + myEncoding = theEncoding; + // return this; + } + /** * {@inheritDoc} */ @@ -192,10 +180,21 @@ public abstract class BaseClient implements IRestfulClient { return mySummary; } + @Override + public void setSummary(SummaryEnum theSummary) { + mySummary = theSummary; + } + public String getUrlBase() { return myUrlBase; } + @Override + public void setFormatParamStyle(RequestFormatParamStyleEnum theRequestFormatParamStyle) { + Validate.notNull(theRequestFormatParamStyle, "theRequestFormatParamStyle must not be null"); + myRequestFormatParamStyle = theRequestFormatParamStyle; + } + T invokeClient(FhirContext theContext, IClientResponseHandler binding, BaseHttpClientInvocation clientInvocation) { return invokeClient(theContext, binding, clientInvocation, false); } @@ -219,10 +218,12 @@ public abstract class BaseClient implements IRestfulClient { Map> params = createExtraParams(); if (clientInvocation instanceof HttpGetClientInvocation) { - if (theEncoding == EncodingEnum.XML) { - params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); - } else if (theEncoding == EncodingEnum.JSON) { - params.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); + if (myRequestFormatParamStyle == RequestFormatParamStyleEnum.SHORT) { + if (theEncoding == EncodingEnum.XML) { + params.put(Constants.PARAM_FORMAT, Collections.singletonList("xml")); + } else if (theEncoding == EncodingEnum.JSON) { + params.put(Constants.PARAM_FORMAT, Collections.singletonList("json")); + } } } @@ -252,7 +253,7 @@ public abstract class BaseClient implements IRestfulClient { addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_CACHE, theCacheControlDirective.isNoCache()); addToCacheControlHeader(b, Constants.CACHE_CONTROL_NO_STORE, theCacheControlDirective.isNoStore()); if (theCacheControlDirective.getMaxResults() != null) { - addToCacheControlHeader(b, Constants.CACHE_CONTROL_MAX_RESULTS+"="+ Integer.toString(theCacheControlDirective.getMaxResults().intValue()), true); + addToCacheControlHeader(b, Constants.CACHE_CONTROL_MAX_RESULTS + "=" + Integer.toString(theCacheControlDirective.getMaxResults().intValue()), true); } if (b.length() > 0) { httpRequest.addHeader(Constants.HEADER_CACHE_CONTROL, b.toString()); @@ -397,6 +398,13 @@ public abstract class BaseClient implements IRestfulClient { return myKeepResponses; } + /** + * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! + */ + public void setKeepResponses(boolean theKeepResponses) { + myKeepResponses = theKeepResponses; + } + /** * Returns the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other @@ -406,6 +414,17 @@ public abstract class BaseClient implements IRestfulClient { return Boolean.TRUE.equals(myPrettyPrint); } + /** + * Sets the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note + * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other + * servers which might implement it). + */ + @Override + public void setPrettyPrint(Boolean thePrettyPrint) { + myPrettyPrint = thePrettyPrint; + // return this; + } + private void keepResponseAndLogIt(boolean theLogRequestAndResponse, IHttpResponse response, String responseString) { if (myKeepResponses) { myLastResponse = response; @@ -438,55 +457,12 @@ public abstract class BaseClient implements IRestfulClient { myDontValidateConformance = theDontValidateConformance; } - /** - * Sets the encoding that will be used on requests. Default is null, which means the client will not - * explicitly request an encoding. (This is perfectly acceptable behaviour according to the FHIR specification. In - * this case, the server will choose which encoding to return, and the client can handle either XML or JSON) - */ - @Override - public void setEncoding(EncodingEnum theEncoding) { - myEncoding = theEncoding; - // return this; - } - - /** - * For now, this is a part of the internal API of HAPI - Use with caution as this method may change! - */ - public void setKeepResponses(boolean theKeepResponses) { - myKeepResponses = theKeepResponses; - } - - /** - * Sets the pretty print flag, which is a request to the server for it to return "pretty printed" responses. Note - * that this is currently a non-standard flag (_pretty) which is supported only by HAPI based servers (and any other - * servers which might implement it). - */ - @Override - public void setPrettyPrint(Boolean thePrettyPrint) { - myPrettyPrint = thePrettyPrint; - // return this; - } - - @Override - public void setSummary(SummaryEnum theSummary) { - mySummary = theSummary; - } - @Override public void unregisterInterceptor(IClientInterceptor theInterceptor) { Validate.notNull(theInterceptor, "Interceptor can not be null"); myInterceptors.remove(theInterceptor); } - static ArrayList> toTypeList(Class thePreferResponseType) { - ArrayList> preferResponseTypes = null; - if (thePreferResponseType != null) { - preferResponseTypes = new ArrayList>(1); - preferResponseTypes.add(thePreferResponseType); - } - return preferResponseTypes; - } - protected final class ResourceResponseHandler implements IClientResponseHandler { private boolean myAllowHtmlResponse; @@ -568,4 +544,13 @@ public abstract class BaseClient implements IRestfulClient { } } + static ArrayList> toTypeList(Class thePreferResponseType) { + ArrayList> preferResponseTypes = null; + if (thePreferResponseType != null) { + preferResponseTypes = new ArrayList>(1); + preferResponseTypes.add(thePreferResponseType); + } + return preferResponseTypes; + } + } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientHeadersR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientHeadersR4Test.java index 9d4fdf78a12..fc690862fdd 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientHeadersR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/ClientHeadersR4Test.java @@ -1,17 +1,13 @@ package ca.uhn.fhir.rest.client; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.context.FhirVersionEnum; import ca.uhn.fhir.rest.api.Constants; import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; import ca.uhn.fhir.util.RandomServerPortProvider; import ca.uhn.fhir.util.TestUtil; -import ca.uhn.fhir.util.VersionUtil; -import org.apache.commons.io.IOUtils; -import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; -import org.apache.http.client.methods.HttpUriRequest; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; @@ -20,9 +16,7 @@ import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.mockito.ArgumentCaptor; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -40,8 +34,9 @@ public class ClientHeadersR4Test { private static Server ourServer; private static String ourServerBase; private static HashMap> ourHeaders; - private static IGenericClient ourClient; + private static HashMap ourParams; private static String ourMethod; + private IGenericClient myClient; @Before public void before() { @@ -49,34 +44,125 @@ public class ClientHeadersR4Test { ourMethod = null; } - private String expectedUserAgent() { - return "HAPI-FHIR/" + VersionUtil.getVersion() + " (FHIR Client; FHIR " + FhirVersionEnum.R4.getFhirVersionString() + "/R4; apache)"; - } - - private byte[] extractBodyAsByteArray(ArgumentCaptor capt) throws IOException { - byte[] body = IOUtils.toByteArray(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(0)).getEntity().getContent()); - return body; - } - - private String extractBodyAsString(ArgumentCaptor capt) throws IOException { - String body = IOUtils.toString(((HttpEntityEnclosingRequestBase) capt.getAllValues().get(0)).getEntity().getContent(), "UTF-8"); - return body; - } - @Test - public void testCreateWithPreferRepresentationServerReturnsResource() throws Exception { + public void testReadXml() { + myClient + .read() + .resource("Patient") + .withId(123L) + .encodedXml() + .execute(); + + assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0)); + assertEquals("xml", ourParams.get(Constants.PARAM_FORMAT)[0]); + } + + @Test + public void testReadXmlNoParam() { + myClient.setFormatParamStyle(RequestFormatParamStyleEnum.NONE); + myClient + .read() + .resource("Patient") + .withId(123L) + .encodedXml() + .execute(); + + assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0)); + assertEquals(null, ourParams.get(Constants.PARAM_FORMAT)); + } + + @Test + public void testReadJson() { + myClient + .read() + .resource("Patient") + .withId(123L) + .encodedJson() + .execute(); + + assertEquals("application/fhir+json;q=1.0, application/json+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0)); + assertEquals("json", ourParams.get(Constants.PARAM_FORMAT)[0]); + } + + @Test + public void testReadJsonNoParam() { + myClient.setFormatParamStyle(RequestFormatParamStyleEnum.NONE); + myClient + .read() + .resource("Patient") + .withId(123L) + .encodedJson() + .execute(); + + assertEquals("application/fhir+json;q=1.0, application/json+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0)); + assertEquals(null, ourParams.get(Constants.PARAM_FORMAT)); + } + + @Test + public void testReadXmlDisable() { + myClient + .read() + .resource("Patient") + .withId(123L) + .encodedXml() + .execute(); + + assertEquals("application/fhir+xml;q=1.0, application/xml+fhir;q=0.9", ourHeaders.get(Constants.HEADER_ACCEPT).get(0)); + assertEquals("xml", ourParams.get(Constants.PARAM_FORMAT)[0]); + } + + @Test + public void testCreateWithPreferRepresentationServerReturnsResource() { final Patient resp1 = new Patient(); resp1.setActive(true); - MethodOutcome resp = ourClient.create().resource(resp1).execute(); + MethodOutcome resp = myClient.create().resource(resp1).execute(); assertNotNull(resp); assertEquals(1, ourHeaders.get(Constants.HEADER_CONTENT_TYPE).size()); assertEquals("application/fhir+xml; charset=UTF-8", ourHeaders.get(Constants.HEADER_CONTENT_TYPE).get(0)); } + @Before + public void beforeCreateClient() { + myClient = ourCtx.newRestfulGenericClient(ourServerBase); + } + + private static class TestServlet extends HttpServlet { + + @Override + protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException { + + if (ourHeaders != null) { + fail(); + } + ourHeaders = new HashMap<>(); + ourParams = new HashMap<>(req.getParameterMap()); + ourMethod = req.getMethod(); + Enumeration names = req.getHeaderNames(); + while (names.hasMoreElements()) { + String nextName = names.nextElement(); + ourHeaders.put(nextName, new ArrayList<>()); + Enumeration values = req.getHeaders(nextName); + while (values.hasMoreElements()) { + ourHeaders.get(nextName).add(values.nextElement()); + } + } + + resp.setStatus(200); + + if (req.getMethod().equals("GET")) { + resp.setContentType("application/json"); + resp.getWriter().append("{\"resourceType\":\"Patient\"}"); + resp.getWriter().close(); + } + + } + + } + @AfterClass public static void afterClassClearContext() { TestUtil.clearAllStaticFieldsForUnitTest(); @@ -94,7 +180,6 @@ public class ClientHeadersR4Test { ourServerBase = "http://localhost:" + myPort + "/fhir/context"; ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); - ourClient = ourCtx.newRestfulGenericClient(ourServerBase); ServletHolder servletHolder = new ServletHolder(); servletHolder.setServlet(new TestServlet()); @@ -105,29 +190,4 @@ public class ClientHeadersR4Test { } - private static class TestServlet extends HttpServlet { - - @Override - protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - - if (ourHeaders != null) { - fail(); - } - ourHeaders = new HashMap<>(); - ourMethod = req.getMethod(); - Enumeration names = req.getHeaderNames(); - while (names.hasMoreElements()) { - String nextName = names.nextElement(); - ourHeaders.put(nextName, new ArrayList()); - Enumeration values = req.getHeaders(nextName); - while (values.hasMoreElements()) { - ourHeaders.get(nextName).add(values.nextElement()); - } - } - - resp.setStatus(200); - } - - } - } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 8d7249421d2..560889f4481 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -171,7 +171,12 @@ When restful reponses tried to return multiple instances of the same response header, some instances were discarded. Thanks to Volker Schmidt for the pull request! - " + + + The REST client now allows for configurable behaviour as to whether a + _format]]> + parameter should be included in requests. + From a3e79b57594eac92106434bb5e0e38f6a7ce09ec Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Thu, 8 Nov 2018 15:28:09 -0500 Subject: [PATCH 09/11] License updates only --- ...ocalContainerEntityManagerFactoryBean.java | 20 +++++++++++++++++++ .../java/ca/uhn/fhir/jpa/dao/DaoConfig.java | 4 ++-- .../fhir/jpa/dao/data/IResourceTableDao.java | 4 ++-- .../auth/AuthorizationInterceptor.java | 4 ++-- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirLocalContainerEntityManagerFactoryBean.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirLocalContainerEntityManagerFactoryBean.java index 335dee945da..e31adf6bbc4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirLocalContainerEntityManagerFactoryBean.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/config/HapiFhirLocalContainerEntityManagerFactoryBean.java @@ -1,5 +1,25 @@ package ca.uhn.fhir.jpa.config; +/*- + * #%L + * HAPI FHIR JPA Server + * %% + * Copyright (C) 2014 - 2018 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.hibernate.cfg.AvailableSettings; import org.hibernate.query.criteria.LiteralHandlingMode; import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java index 7727d2110aa..5bc6c0879f1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/DaoConfig.java @@ -23,9 +23,9 @@ import java.util.*; * 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. diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java index 1370ae9daba..e852e8faf82 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/data/IResourceTableDao.java @@ -21,9 +21,9 @@ import java.util.Map; * 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. diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java index 68aca58273c..fc04125952d 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java @@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth; * 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. From 01c7618867d3519a4ed6f0b6213e190e50115b6e Mon Sep 17 00:00:00 2001 From: James Agnew Date: Thu, 8 Nov 2018 15:46:49 -0500 Subject: [PATCH 10/11] Failing test cleanup --- .../rest/client/GenericClientDstu2Test.java | 5 + .../rest/client/OperationClientR4Test.java | 152 +++++++++--------- 2 files changed, 81 insertions(+), 76 deletions(-) diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java index 2c618758489..f6d88a86b8e 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/client/GenericClientDstu2Test.java @@ -2651,6 +2651,11 @@ public class GenericClientDstu2Test { // nothing } + @Override + public void setFormatParamStyle(RequestFormatParamStyleEnum theRequestFormatParamStyle) { + // nothing + } + @Override public EncodingEnum getEncoding() { // TODO Auto-generated method stub diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/OperationClientR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/OperationClientR4Test.java index 94d686d93da..b485105735b 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/OperationClientR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/OperationClientR4Test.java @@ -1,41 +1,45 @@ package ca.uhn.fhir.rest.client; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.annotation.Operation; +import ca.uhn.fhir.rest.annotation.OperationParam; +import ca.uhn.fhir.rest.api.Constants; +import ca.uhn.fhir.rest.client.api.IBasicClient; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum; +import ca.uhn.fhir.rest.param.*; +import ca.uhn.fhir.util.TestUtil; +import com.google.common.base.Charsets; +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.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicStatusLine; +import org.hl7.fhir.r4.model.IdType; +import org.hl7.fhir.r4.model.Parameters; +import org.hl7.fhir.r4.model.StringType; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; import java.io.InputStream; import java.io.StringReader; import java.nio.charset.Charset; import java.util.List; -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.HttpClient; -import org.apache.http.client.methods.*; -import org.apache.http.message.BasicHeader; -import org.apache.http.message.BasicStatusLine; -import org.hl7.fhir.r4.model.IdType; -import org.hl7.fhir.r4.model.Parameters; -import org.hl7.fhir.r4.model.StringType; -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 com.google.common.base.Charsets; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.rest.annotation.Operation; -import ca.uhn.fhir.rest.annotation.OperationParam; -import ca.uhn.fhir.rest.api.Constants; -import ca.uhn.fhir.rest.client.api.*; -import ca.uhn.fhir.rest.param.*; -import ca.uhn.fhir.util.TestUtil; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class OperationClientR4Test { @@ -48,13 +52,6 @@ public class OperationClientR4Test { private ArgumentCaptor capt; private IGenericClient ourGenClient; - - @AfterClass - public static void afterClassClearContext() { - TestUtil.clearAllStaticFieldsForUnitTest(); - } - - @Before public void before() throws Exception { ourCtx = FhirContext.forR4(); @@ -64,7 +61,7 @@ public class OperationClientR4Test { ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER); ourHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs()); - + Parameters outParams = new Parameters(); outParams.addParameter().setName("FOO"); final String retVal = ourCtx.newXmlParser().encodeResourceToString(outParams); @@ -75,7 +72,7 @@ public class OperationClientR4Test { when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8")); when(ourHttpResponse.getEntity().getContent()).thenAnswer(new Answer() { @Override - public InputStream answer(InvocationOnMock theInvocation) throws Throwable { + public InputStream answer(InvocationOnMock theInvocation) { return new ReaderInputStream(new StringReader(retVal), Charset.forName("UTF-8")); } }); @@ -95,10 +92,10 @@ public class OperationClientR4Test { .execute(); Parameters response = ourAnnClient.nonrepeating(new StringParam("str"), new TokenParam("sys", "val")); assertEquals("FOO", response.getParameter().get(0).getName()); - + HttpPost value = (HttpPost) capt.getAllValues().get(0); - String requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent(), Charsets.UTF_8); - IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent()); + String requestBody = IOUtils.toString(value.getEntity().getContent(), Charsets.UTF_8); + IOUtils.closeQuietly(value.getEntity().getContent()); ourLog.info(requestBody); Parameters request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody); assertEquals("http://foo/$nonrepeating", value.getURI().toASCIIString()); @@ -110,7 +107,7 @@ public class OperationClientR4Test { } @Test - public void testNonRepeatingGenericUsingUrl() throws Exception { + public void testNonRepeatingGenericUsingUrl() { ourGenClient .operation() .onServer() @@ -121,14 +118,13 @@ public class OperationClientR4Test { .execute(); Parameters response = ourAnnClient.nonrepeating(new StringParam("str"), new TokenParam("sys", "val")); assertEquals("FOO", response.getParameter().get(0).getName()); - + HttpGet value = (HttpGet) capt.getAllValues().get(0); assertEquals("http://foo/$nonrepeating?valstr=str&valtok=sys2%7Cval2", value.getURI().toASCIIString()); } - @Test - public void testOperationOnInstanceWithIncompleteInstanceId() throws Exception { + public void testOperationOnInstanceWithIncompleteInstanceId() { try { ourGenClient .operation() @@ -146,10 +142,10 @@ public class OperationClientR4Test { public void testNonRepeatingUsingParameters() throws Exception { Parameters response = ourAnnClient.nonrepeating(new StringParam("str"), new TokenParam("sys", "val")); assertEquals("FOO", response.getParameter().get(0).getName()); - + HttpPost value = (HttpPost) capt.getAllValues().get(0); - String requestBody = IOUtils.toString(((HttpPost) value).getEntity().getContent(), Charsets.UTF_8); - IOUtils.closeQuietly(((HttpPost) value).getEntity().getContent()); + String requestBody = IOUtils.toString(value.getEntity().getContent(), Charsets.UTF_8); + IOUtils.closeQuietly(value.getEntity().getContent()); ourLog.info(requestBody); Parameters request = ourCtx.newXmlParser().parseResource(Parameters.class, requestBody); assertEquals("http://foo/$nonrepeating", value.getURI().toASCIIString()); @@ -160,48 +156,52 @@ public class OperationClientR4Test { assertEquals("sys|val", ((StringType) request.getParameter().get(1).getValue()).getValue()); } - - public interface IOpClient extends IBasicClient { + public interface IOpClient extends IBasicClient { @Operation(name = "$andlist", idempotent = true) - public Parameters andlist( - //@formatter:off - @OperationParam(name="valstr", max=10) StringAndListParam theValStr, - @OperationParam(name="valtok", max=10) TokenAndListParam theValTok - //@formatter:on + Parameters andlist( + //@formatter:off + @OperationParam(name = "valstr", max = 10) StringAndListParam theValStr, + @OperationParam(name = "valtok", max = 10) TokenAndListParam theValTok + //@formatter:on ); @Operation(name = "$andlist-withnomax", idempotent = true) - public Parameters andlistWithNoMax( - //@formatter:off - @OperationParam(name="valstr") StringAndListParam theValStr, - @OperationParam(name="valtok") TokenAndListParam theValTok - //@formatter:on + Parameters andlistWithNoMax( + //@formatter:off + @OperationParam(name = "valstr") StringAndListParam theValStr, + @OperationParam(name = "valtok") TokenAndListParam theValTok + //@formatter:on ); @Operation(name = "$nonrepeating", idempotent = true) - public Parameters nonrepeating( - //@formatter:off - @OperationParam(name="valstr") StringParam theValStr, - @OperationParam(name="valtok") TokenParam theValTok - //@formatter:on + Parameters nonrepeating( + //@formatter:off + @OperationParam(name = "valstr") StringParam theValStr, + @OperationParam(name = "valtok") TokenParam theValTok + //@formatter:on ); @Operation(name = "$orlist", idempotent = true) - public Parameters orlist( - //@formatter:off - @OperationParam(name="valstr", max=10) List theValStr, - @OperationParam(name="valtok", max=10) List theValTok - //@formatter:on + Parameters orlist( + //@formatter:off + @OperationParam(name = "valstr", max = 10) List theValStr, + @OperationParam(name = "valtok", max = 10) List theValTok + //@formatter:on ); @Operation(name = "$orlist-withnomax", idempotent = true) - public Parameters orlistWithNoMax( - //@formatter:off - @OperationParam(name="valstr") List theValStr, - @OperationParam(name="valtok") List theValTok - //@formatter:on + Parameters orlistWithNoMax( + //@formatter:off + @OperationParam(name = "valstr") List theValStr, + @OperationParam(name = "valtok") List theValTok + //@formatter:on ); } + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } } From b32e232eb2f037496c39019c6d31ca54857438cd Mon Sep 17 00:00:00 2001 From: James Agnew Date: Thu, 8 Nov 2018 17:10:39 -0500 Subject: [PATCH 11/11] A bit of client tweaking --- .../fhir/rest/gclient/IOperationUntyped.java | 2 +- ...rationUntypedWithInputAndPartialOutput.java | 2 ++ .../fhir/rest/client/impl/GenericClient.java | 7 ++++--- .../rest/client/OperationClientR4Test.java | 18 ++++++++++++++++++ 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntyped.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntyped.java index dff17e286e1..40155444efc 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntyped.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntyped.java @@ -34,7 +34,7 @@ public interface IOperationUntyped { * @param theParameters The parameters to use as input. May also be null if the operation * does not require any input parameters. */ - IOperationUntypedWithInput withParameters(T theParameters); + IOperationUntypedWithInputAndPartialOutput withParameters(T theParameters); /** * The operation does not require any input parameters diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInputAndPartialOutput.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInputAndPartialOutput.java index ace3562a8c9..71594f48c47 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInputAndPartialOutput.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/gclient/IOperationUntypedWithInputAndPartialOutput.java @@ -38,6 +38,8 @@ public interface IOperationUntypedWithInputAndPartialOutput andParameter(String theName, IBase theValue); /** + * Adds a URL parameter to the request. + * * Use chained method calls to construct a Parameters input. This form is a convenience * in order to allow simple method chaining to be used to build up a parameters * resource for the input of an operation without needing to manually construct one. diff --git a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java index 991e87f6907..b9705fc3534 100644 --- a/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java +++ b/hapi-fhir-client/src/main/java/ca/uhn/fhir/rest/client/impl/GenericClient.java @@ -1276,7 +1276,7 @@ public class GenericClient extends BaseClient implements IGenericClient { @SuppressWarnings("unchecked") @Override - public IOperationUntypedWithInput withNoParameters(Class theOutputParameterType) { + public IOperationUntypedWithInputAndPartialOutput withNoParameters(Class theOutputParameterType) { Validate.notNull(theOutputParameterType, "theOutputParameterType may not be null"); RuntimeResourceDefinition def = myContext.getResourceDefinition(theOutputParameterType); if (def == null) { @@ -1307,9 +1307,10 @@ public class GenericClient extends BaseClient implements IGenericClient { @SuppressWarnings({"unchecked"}) @Override - public IOperationUntypedWithInput withParameters(IBaseParameters theParameters) { + public IOperationUntypedWithInputAndPartialOutput withParameters(IBaseParameters theParameters) { Validate.notNull(theParameters, "theParameters can not be null"); myParameters = theParameters; + myParametersDef = myContext.getResourceDefinition(theParameters.getClass()); return this; } @@ -1445,7 +1446,7 @@ public class GenericClient extends BaseClient implements IGenericClient { OutcomeResponseHandler binding = new OutcomeResponseHandler(myPrefer); - Map> params = new HashMap>(); + Map> params = new HashMap<>(); return invoke(params, binding, invocation); } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/OperationClientR4Test.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/OperationClientR4Test.java index b485105735b..6f658fe5ec6 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/OperationClientR4Test.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/client/OperationClientR4Test.java @@ -123,6 +123,24 @@ public class OperationClientR4Test { assertEquals("http://foo/$nonrepeating?valstr=str&valtok=sys2%7Cval2", value.getURI().toASCIIString()); } + @Test + public void testNonRepeatingGenericUsingUrl2() { + ourGenClient + .operation() + .onServer() + .named("nonrepeating") + .withParameters(new Parameters()) + .andSearchParameter("valstr", new StringParam("str")) + .andSearchParameter("valtok", new TokenParam("sys2", "val2")) + .useHttpGet() + .execute(); + Parameters response = ourAnnClient.nonrepeating(new StringParam("str"), new TokenParam("sys", "val")); + assertEquals("FOO", response.getParameter().get(0).getName()); + + HttpGet value = (HttpGet) capt.getAllValues().get(0); + assertEquals("http://foo/$nonrepeating?valstr=str&valtok=sys2%7Cval2", value.getURI().toASCIIString()); + } + @Test public void testOperationOnInstanceWithIncompleteInstanceId() { try {