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.
+
+
+