Unescape Read ID and Version IDs

This commit is contained in:
jamesagnew 2015-02-10 14:57:16 -05:00
parent 9828d27eec
commit 46b6979125
4 changed files with 91 additions and 38 deletions

View File

@ -30,6 +30,7 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.net.URLDecoder;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -51,6 +52,7 @@ import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.exception.ExceptionUtils; 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.exceptions.NotModifiedException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.util.ReflectionUtil; import ca.uhn.fhir.util.ReflectionUtil;
import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.util.VersionUtil; import ca.uhn.fhir.util.VersionUtil;
public class RestfulServer extends HttpServlet { 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 * 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. * (metadata) statement if one has been explicitly defined.
* <p> * <p>
* By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or set to <code>null</code> * By default, the ServerConformanceProvider for the declared version of FHIR is used, but this can be changed, or
* to use the appropriate one for the given FHIR version. * set to <code>null</code> to use the appropriate one for the given FHIR version.
* </p> * </p>
*/ */
public Object getServerConformanceProvider() { public Object getServerConformanceProvider() {
@ -539,7 +542,7 @@ public class RestfulServer extends HttpServlet {
if (nextString.startsWith("_")) { if (nextString.startsWith("_")) {
operation = nextString; operation = nextString;
} else { } 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) { if (id == null) {
throw new InvalidRequestException("Don't know how to handle request path: " + requestPath); 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 { } else {
operation = Constants.PARAM_HISTORY; operation = Constants.PARAM_HISTORY;
} }
@ -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 * Returns the server conformance provider, which is the provider that is used to generate the server's conformance
* (metadata) statement. * (metadata) statement.
* <p> * <p>
* By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can be changed, or set to <code>null</code> * By default, the ServerConformanceProvider implementation for the declared version of FHIR is used, but this can
* if you do not wish to export a conformance statement. * be changed, or set to <code>null</code> if you do not wish to export a conformance statement.
* </p> * </p>
* Note that this method can only be called before the server is initialized. * Note that this method can only be called before the server is initialized.
* *
@ -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 * Allows users of RestfulServer to override the getRequestPath method to let them build their custom request path
* implementation * implementation
* *
* @param requestFullPath the full request path * @param requestFullPath
* @param servletContextPath the servelet context path * the full request path
* @param servletPath the servelet path * @param servletContextPath
* the servelet context path
* @param servletPath
* the servelet path
* @return created resource path * @return created resource path
*/ */
protected String getRequestPath(String requestFullPath, String servletContextPath, String servletPath) { protected String getRequestPath(String requestFullPath, String servletContextPath, String servletPath) {

View File

@ -1,7 +1,9 @@
package ca.uhn.fhir.util; package ca.uhn.fhir.util;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLDecoder;
/* /*
* #%L * #%L
@ -27,8 +29,8 @@ public class UrlUtil {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(UrlUtil.class); 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 * Resolve a relative URL - THIS METHOD WILL NOT FAIL but will log a warning and return theEndpoint if the input is
* and return theEndpoint if the input is invalid. * invalid.
*/ */
public static String constructAbsoluteUrl(String theBase, String theEndpoint) { public static String constructAbsoluteUrl(String theBase, String theEndpoint) {
if (theEndpoint == null) { if (theEndpoint == null) {
@ -78,7 +80,7 @@ public class UrlUtil {
} }
if (theExtensionUrl.length() > parentLastSlashIdx) { if (theExtensionUrl.length() > parentLastSlashIdx) {
return theExtensionUrl.substring(parentLastSlashIdx+1); return theExtensionUrl.substring(parentLastSlashIdx + 1);
} }
return theExtensionUrl; return theExtensionUrl;
@ -124,4 +126,18 @@ public class UrlUtil {
return true; 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);
}
}
} }

View File

@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.stringContainsInOrder; import static org.hamcrest.Matchers.stringContainsInOrder;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import java.net.URLEncoder;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
@ -20,6 +21,8 @@ import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import com.google.common.net.UrlEscapers;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.BaseResource; import ca.uhn.fhir.model.api.BaseResource;
import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.IResource;
@ -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 @AfterClass
public static void afterClass() throws Exception { public static void afterClass() throws Exception {
ourServer.stop(); ourServer.stop();
@ -166,12 +188,12 @@ public class ReadTest {
ourPort = PortUtil.findFreePort(); ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort); ourServer = new Server(ourPort);
DummyProvider patientProvider = new DummyProvider(); PatientProvider patientProvider = new PatientProvider();
ServletHandler proxyHandler = new ServletHandler(); ServletHandler proxyHandler = new ServletHandler();
RestfulServer servlet = new RestfulServer(); RestfulServer servlet = new RestfulServer();
ourCtx = servlet.getFhirContext(); ourCtx = servlet.getFhirContext();
servlet.setResourceProviders(patientProvider, new DummyBinaryProvider(), new OrganizationProviderWithAbstractReturnType()); servlet.setResourceProviders(patientProvider, new BinaryProvider(), new OrganizationProviderWithAbstractReturnType());
ServletHolder servletHolder = new ServletHolder(servlet); ServletHolder servletHolder = new ServletHolder(servlet);
proxyHandler.addServletWithMapping(servletHolder, "/*"); proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler); ourServer.setHandler(proxyHandler);
@ -187,7 +209,7 @@ public class ReadTest {
/** /**
* Created by dsotnikov on 2/25/2014. * Created by dsotnikov on 2/25/2014.
*/ */
public static class DummyProvider implements IResourceProvider { public static class PatientProvider implements IResourceProvider {
@Read(version = true) @Read(version = true)
public Patient read(@IdParam IdDt theId) { public Patient read(@IdParam IdDt theId) {
@ -210,7 +232,7 @@ public class ReadTest {
public static class OrganizationProviderWithAbstractReturnType implements IResourceProvider { public static class OrganizationProviderWithAbstractReturnType implements IResourceProvider {
@Read(version = true) @Read(version = true)
public BaseResource findPatient(@IdParam IdDt theId) { public BaseResource findOrganization(@IdParam IdDt theId) {
Organization org = new Organization(); Organization org = new Organization();
org.addIdentifier(theId.getIdPart(), theId.getVersionIdPart()); org.addIdentifier(theId.getIdPart(), theId.getVersionIdPart());
org.setId("Organization/1/_history/1"); org.setId("Organization/1/_history/1");
@ -227,7 +249,7 @@ public class ReadTest {
/** /**
* Created by dsotnikov on 2/25/2014. * Created by dsotnikov on 2/25/2014.
*/ */
public static class DummyBinaryProvider implements IResourceProvider { public static class BinaryProvider implements IResourceProvider {
@Read(version = true) @Read(version = true)
public Binary findPatient(@IdParam IdDt theId) { public Binary findPatient(@IdParam IdDt theId) {

View File

@ -27,12 +27,21 @@
<b>Important Note: </b> <b>Important Note: </b>
This implementation uses a fairly simple table design, with a This implementation uses a fairly simple table design, with a
single table being used to hold resource bodies (which are stored as 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 history details, etc. This design is only one of many possible ways
of designing a FHIR server so it is worth considering whether it of designing a FHIR server so it is worth considering whether it
is appropriate for the problem you are trying to solve. is appropriate for the problem you are trying to solve.
</p> </p>
<subsection name="Getting Started">
<p>
The easiest way to get started with HAPI's JPA server module is
to begin with the example project.
</p>
</subsection>
</section> </section>
</body> </body>