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 c4d9084c856..a1a08b07dfe 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 @@ -30,6 +30,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.net.URLDecoder; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; @@ -51,6 +52,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -91,6 +93,7 @@ import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; import ca.uhn.fhir.rest.server.exceptions.NotModifiedException; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.util.ReflectionUtil; +import ca.uhn.fhir.util.UrlUtil; import ca.uhn.fhir.util.VersionUtil; public class RestfulServer extends HttpServlet { @@ -385,8 +388,8 @@ public class RestfulServer extends HttpServlet { * Returns the server conformance provider, which is the provider that is used to generate the server's conformance * (metadata) statement if one has been explicitly defined. *

- * By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or set to null - * to use the appropriate one for the given FHIR version. + * By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or + * set to null to use the appropriate one for the given FHIR version. *

*/ public Object getServerConformanceProvider() { @@ -539,7 +542,7 @@ public class RestfulServer extends HttpServlet { if (nextString.startsWith("_")) { operation = nextString; } else { - id = new IdDt(resourceName, nextString); + id = new IdDt(resourceName, UrlUtil.unescape(nextString)); } } @@ -551,7 +554,7 @@ public class RestfulServer extends HttpServlet { if (id == null) { throw new InvalidRequestException("Don't know how to handle request path: " + requestPath); } - id = new IdDt(resourceName + "/" + id.getIdPart() + "/_history/" + versionString); + id = new IdDt(resourceName, id.getIdPart(), UrlUtil.unescape(versionString)); } else { operation = Constants.PARAM_HISTORY; } @@ -797,7 +800,7 @@ public class RestfulServer extends HttpServlet { } findResourceMethods(getServerProfilesProvider()); - + Object confProvider = getServerConformanceProvider(); if (confProvider == null) { confProvider = myFhirContext.getVersion().createServerConformanceProvider(this); @@ -984,8 +987,8 @@ public class RestfulServer extends HttpServlet { * Returns the server conformance provider, which is the provider that is used to generate the server's conformance * (metadata) statement. *

- * By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can be changed, or set to null - * if you do not wish to export a conformance statement. + * By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can + * be changed, or set to null if you do not wish to export a conformance statement. *

* Note that this method can only be called before the server is initialized. * @@ -1148,13 +1151,13 @@ public class RestfulServer extends HttpServlet { List includedResources = new ArrayList(); Set addedResourceIds = new HashSet(); - + for (IResource next : theResult) { if (next.getId().isEmpty() == false) { addedResourceIds.add(next.getId()); } } - + for (IResource next : theResult) { Set containedIds = new HashSet(); @@ -1214,7 +1217,7 @@ public class RestfulServer extends HttpServlet { } while (references.isEmpty() == false); bundle.addResource(next, theContext, theServerBase); - + } /* @@ -1534,9 +1537,12 @@ public class RestfulServer extends HttpServlet { * Allows users of RestfulServer to override the getRequestPath method to let them build their custom request path * implementation * - * @param requestFullPath the full request path - * @param servletContextPath the servelet context path - * @param servletPath the servelet path + * @param requestFullPath + * the full request path + * @param servletContextPath + * the servelet context path + * @param servletPath + * the servelet path * @return created resource path */ protected String getRequestPath(String requestFullPath, String servletContextPath, String servletPath) { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java index 1bdc3435326..05548913df2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/UrlUtil.java @@ -1,7 +1,9 @@ package ca.uhn.fhir.util; +import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; +import java.net.URLDecoder; /* * #%L @@ -25,10 +27,10 @@ import java.net.URL; public class UrlUtil { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UrlUtil.class); - + /** - * Resolve a relative URL - THIS METHOD WILL NOT FAIL but will log a warning - * and return theEndpoint if the input is invalid. + * Resolve a relative URL - THIS METHOD WILL NOT FAIL but will log a warning and return theEndpoint if the input is + * invalid. */ public static String constructAbsoluteUrl(String theBase, String theEndpoint) { if (theEndpoint == null) { @@ -40,7 +42,7 @@ public class UrlUtil { if (theBase == null) { return theEndpoint; } - + try { return new URL(new URL(theBase), theEndpoint).toString(); } catch (MalformedURLException e) { @@ -48,7 +50,7 @@ public class UrlUtil { return theEndpoint; } } - + public static boolean isAbsolute(String theValue) { String value = theValue.toLowerCase(); return value.startsWith("http://") || value.startsWith("https://"); @@ -61,26 +63,26 @@ public class UrlUtil { if (theExtensionUrl == null) { return theExtensionUrl; } - + int parentLastSlashIdx = theParentExtensionUrl.lastIndexOf('/'); int childLastSlashIdx = theExtensionUrl.lastIndexOf('/'); - + if (parentLastSlashIdx == -1 || childLastSlashIdx == -1) { return theExtensionUrl; } - + if (parentLastSlashIdx != childLastSlashIdx) { return theExtensionUrl; } - + if (!theParentExtensionUrl.substring(0, parentLastSlashIdx).equals(theExtensionUrl.substring(0, parentLastSlashIdx))) { return theExtensionUrl; } - + if (theExtensionUrl.length() > parentLastSlashIdx) { - return theExtensionUrl.substring(parentLastSlashIdx+1); + return theExtensionUrl.substring(parentLastSlashIdx + 1); } - + return theExtensionUrl; } @@ -88,7 +90,7 @@ public class UrlUtil { if (theUrl == null || theUrl.length() < 8) { return false; } - + String url = theUrl.toLowerCase(); if (url.charAt(0) != 'h') { return false; @@ -113,7 +115,7 @@ public class UrlUtil { } else { return false; } - + if (url.charAt(slashOffset) != '/') { return false; } @@ -123,5 +125,19 @@ public class UrlUtil { return true; } - + + public static String unescape(String theString) { + if (theString == null) { + return null; + } + if (theString.indexOf('%') == -1) { + return theString; + } + try { + return URLDecoder.decode(theString, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new Error("UTF-8 not supported, this shouldn't happen", e); + } + } + } diff --git a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ReadTest.java b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ReadTest.java index efc2fe2249f..b6eb9f1c2df 100644 --- a/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ReadTest.java +++ b/hapi-fhir-structures-dstu/src/test/java/ca/uhn/fhir/rest/server/ReadTest.java @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.stringContainsInOrder; import static org.junit.Assert.*; +import java.net.URLEncoder; import java.util.concurrent.TimeUnit; import org.apache.commons.io.IOUtils; @@ -20,6 +21,8 @@ import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; +import com.google.common.net.UrlEscapers; + import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.BaseResource; import ca.uhn.fhir.model.api.IResource; @@ -50,7 +53,7 @@ public class ReadTest { String responseContent = IOUtils.toString(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info(responseContent); - + assertEquals(200, status.getStatusLine().getStatusCode()); IdentifierDt dt = ourCtx.newXmlParser().parseResource(Patient.class, responseContent).getIdentifierFirstRep(); @@ -60,7 +63,7 @@ public class ReadTest { Header cl = status.getFirstHeader(Constants.HEADER_CONTENT_LOCATION_LC); assertNotNull(cl); assertEquals("http://localhost:" + ourPort + "/Patient/1/_history/1", cl.getValue()); - + assertThat(responseContent, stringContainsInOrder("1", "\"")); assertThat(responseContent, not(stringContainsInOrder("1", "\"", "1"))); } @@ -72,7 +75,7 @@ public class ReadTest { String responseContent = IOUtils.toString(status.getEntity().getContent()); IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info(responseContent); - + assertEquals(200, status.getStatusLine().getStatusCode()); IdentifierDt dt = ourCtx.newJsonParser().parseResource(Patient.class, responseContent).getIdentifierFirstRep(); @@ -82,7 +85,7 @@ public class ReadTest { Header cl = status.getFirstHeader(Constants.HEADER_CONTENT_LOCATION_LC); assertNotNull(cl); assertEquals("http://localhost:" + ourPort + "/Patient/1/_history/1", cl.getValue()); - + assertThat(responseContent, stringContainsInOrder("1", "\"")); assertThat(responseContent, not(stringContainsInOrder("1", "\"", "1"))); } @@ -156,6 +159,25 @@ public class ReadTest { } } + @Test + public void testReadWithEscapedCharsInId() throws Exception { + String id = "ABC!@#$%DEF"; + String idEscaped = URLEncoder.encode(id, "UTF-8"); + + String vid = "GHI:/:/JKL"; + String vidEscaped = URLEncoder.encode(vid, "UTF-8"); + + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/" + idEscaped + "/_history/" + vidEscaped); + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + + assertEquals(200, status.getStatusLine().getStatusCode()); + IdentifierDt dt = ourCtx.newXmlParser().parseResource(Patient.class, responseContent).getIdentifierFirstRep(); + assertEquals(id, dt.getSystem().getValueAsString()); + assertEquals(vid, dt.getValue().getValueAsString()); + } + @AfterClass public static void afterClass() throws Exception { ourServer.stop(); @@ -166,12 +188,12 @@ public class ReadTest { ourPort = PortUtil.findFreePort(); ourServer = new Server(ourPort); - DummyProvider patientProvider = new DummyProvider(); + PatientProvider patientProvider = new PatientProvider(); ServletHandler proxyHandler = new ServletHandler(); RestfulServer servlet = new RestfulServer(); ourCtx = servlet.getFhirContext(); - servlet.setResourceProviders(patientProvider, new DummyBinaryProvider(), new OrganizationProviderWithAbstractReturnType()); + servlet.setResourceProviders(patientProvider, new BinaryProvider(), new OrganizationProviderWithAbstractReturnType()); ServletHolder servletHolder = new ServletHolder(servlet); proxyHandler.addServletWithMapping(servletHolder, "/*"); ourServer.setHandler(proxyHandler); @@ -187,7 +209,7 @@ public class ReadTest { /** * Created by dsotnikov on 2/25/2014. */ - public static class DummyProvider implements IResourceProvider { + public static class PatientProvider implements IResourceProvider { @Read(version = true) public Patient read(@IdParam IdDt theId) { @@ -210,7 +232,7 @@ public class ReadTest { public static class OrganizationProviderWithAbstractReturnType implements IResourceProvider { @Read(version = true) - public BaseResource findPatient(@IdParam IdDt theId) { + public BaseResource findOrganization(@IdParam IdDt theId) { Organization org = new Organization(); org.addIdentifier(theId.getIdPart(), theId.getVersionIdPart()); org.setId("Organization/1/_history/1"); @@ -227,7 +249,7 @@ public class ReadTest { /** * Created by dsotnikov on 2/25/2014. */ - public static class DummyBinaryProvider implements IResourceProvider { + public static class BinaryProvider implements IResourceProvider { @Read(version = true) public Binary findPatient(@IdParam IdDt theId) { diff --git a/src/site/xdoc/doc_jpa.xml b/src/site/xdoc/doc_jpa.xml index b10aa48f5d5..62a4a742a76 100644 --- a/src/site/xdoc/doc_jpa.xml +++ b/src/site/xdoc/doc_jpa.xml @@ -27,12 +27,21 @@ Important Note: This implementation uses a fairly simple table design, with a single table being used to hold resource bodies (which are stored as - GZipped CLOBs) and a set of tables to hold search indexes, tags, + CLOBs, optionally GZipped to save space) and a set of tables to hold search indexes, tags, history details, etc. This design is only one of many possible ways of designing a FHIR server so it is worth considering whether it is appropriate for the problem you are trying to solve.

+ + + +

+ The easiest way to get started with HAPI's JPA server module is + to begin with the example project. + +

+