Merge branch 'master' into hapi3_refactor

This commit is contained in:
James Agnew 2017-07-13 12:02:27 -04:00
commit a92ace2e0d
17 changed files with 1317 additions and 443 deletions

View File

@ -0,0 +1,30 @@
package example;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.dstu3.model.OperationOutcome;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
public class PatchExamples {
//START SNIPPET: patch
@Patch
public OperationOutcome patientPatch(@IdParam IdType theId, PatchTypeEnum thePatchType, @ResourceParam String theBody) {
if (thePatchType == PatchTypeEnum.JSON_PATCH) {
// do something
}
if (thePatchType == PatchTypeEnum.XML_PATCH) {
// do something
}
OperationOutcome retVal = new OperationOutcome();
retVal.getText().setDivAsString("<div>OK</div>");
return retVal;
}
//END SNIPPET: patch
}

View File

@ -141,7 +141,7 @@ public class ServletExamples {
config.addExposedHeader("Location");
config.addExposedHeader("Content-Location");
config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
// Create the interceptor and register it
CorsInterceptor interceptor = new CorsInterceptor(config);

View File

@ -0,0 +1,88 @@
var selectedLines = new Array();
function updateHighlightedLine() {
updateHighlightedLineTo(window.location.hash);
}
function updateHighlightedLineTo(theNewHash) {
for (var next in selectedLines) {
document.getElementById('line' + selectedLines[next]).className = '';
document.getElementById('anchor' + selectedLines[next]).className = 'lineAnchor';
}
selectedLines = new Array();
var line = -1;
if (theNewHash && theNewHash.match('L[0-9]+-L[0-9]+')) {
var dashIndex = theNewHash.indexOf('-');
var start = parseInt(theNewHash.substring(2, dashIndex));
var end = parseInt(theNewHash.substring(dashIndex+2));
for (var i = start; i <= end; i++) {
selectedLines.push(i);
}
} else if (theNewHash && theNewHash.match('L[0-9]+')) {
var line = parseInt(theNewHash.substring(2));
selectedLines.push(line);
}
for (var next in selectedLines) {
// Prevent us from scrolling to the selected line
document.getElementById('L' + selectedLines[next]).name = '';
// Select the line number column
document.getElementById('line' + selectedLines[next]).className = 'selectedLine';
// Select the response body column
document.getElementById('anchor' + selectedLines[next]).className = 'lineAnchor selectedLine';
}
selectedLine = line;
}
(function() {
'use strict';
updateHighlightedLine();
window.onhashchange = updateHighlightedLine;
/* bail out if user is testing a version of this script via Greasemonkey or Tampermonkey */
if (window.HAPI_ResponseHighlighter_userscript) {
console.log("HAPI ResponseHighlighter: userscript detected - not executing embedded script");
return;
}
/* adds hyperlinks and CSS styles to dates and UUIDs (e.g. to enable user-select: all) */
const logicalReferenceRegex = /^[A-Z][A-Za-z]+\/[0-9]+$/;
const dateTimeRegex = /^-?[0-9]{4}(-(0[1-9]|1[0-2])(-(0[0-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]+)?(Z|(\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?$/; // from the spec - https://www.hl7.org/fhir/datatypes.html#datetime
const uuidRegex = /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/;
const allQuotes = document.querySelectorAll(".hlQuot");
for (var i = 0; i < allQuotes.length; i++) {
const quote = allQuotes[i];
const text = quote.textContent.substr(1, quote.textContent.length - 2 /* remove quotes */);
const absHyperlink = text.startsWith("http://") || text.startsWith("https://");
const relHyperlink = text.match(logicalReferenceRegex);
const uuid = text.match(uuidRegex);
const dateTime = text.match(dateTimeRegex);
if (absHyperlink || relHyperlink) {
const link = document.createElement("a");
const href = absHyperlink ? text : "FHIR_BASE/" + text;
link.setAttribute("href", href);
link.textContent = '"' + text + '"';
quote.textContent = "";
quote.appendChild(link);
}
if (uuid || dateTime) {
const span = document.createElement("span");
span.setAttribute("class", uuid ? "uuid" : "dateTime");
span.textContent = text;
quote.textContent = "";
quote.appendChild(document.createTextNode('"'));
quote.appendChild(span);
quote.appendChild(document.createTextNode('"'));
}
}
})();

View File

@ -155,14 +155,18 @@ public class TestRestfulServer extends RestfulServer {
config.addAllowedOrigin("*");
config.addExposedHeader("Location");
config.addExposedHeader("Content-Location");
config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
registerInterceptor(corsInterceptor);
/*
* We want to format the response using nice HTML if it's a browser, since this
* makes things a little easier for testers.
*/
registerInterceptor(new ResponseHighlighterInterceptor());
ResponseHighlighterInterceptor responseHighlighterInterceptor = new ResponseHighlighterInterceptor();
responseHighlighterInterceptor.setShowRequestHeaders(false);
responseHighlighterInterceptor.setShowResponseHeaders(true);
registerInterceptor(responseHighlighterInterceptor);
registerInterceptor(new BanUnsupportedHttpMethodsInterceptor());
/*

View File

@ -43,7 +43,8 @@ public class BanUnsupportedHttpMethodsInterceptor extends InterceptorAdapter {
myAllowedMethods.add(RequestTypeEnum.OPTIONS);
myAllowedMethods.add(RequestTypeEnum.DELETE);
myAllowedMethods.add(RequestTypeEnum.PUT);
myAllowedMethods.add(RequestTypeEnum.POST);
myAllowedMethods.add(RequestTypeEnum.POST);
myAllowedMethods.add(RequestTypeEnum.PATCH);
myAllowedMethods.add(RequestTypeEnum.HEAD);
}

View File

@ -1,5 +1,6 @@
package ca.uhn.fhir.rest.server.interceptor;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -24,25 +25,24 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
*/
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.io.InputStream;
import java.util.*;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.method.RequestDetails;
import ca.uhn.fhir.rest.server.*;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.RestfulServerUtils.ResponseEncoding;
import ca.uhn.fhir.rest.server.exceptions.*;
import ca.uhn.fhir.util.UrlUtil;
/**
@ -62,120 +62,165 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
* requesting _format=json or xml so eventually this parameter should be removed
*/
public static final String PARAM_RAW = "_raw";
public static final String PARAM_RAW_TRUE = "true";
public static final String PARAM_TRUE = "true";
private String format(String theResultBody, EncodingEnum theEncodingEnum) {
private boolean myShowRequestHeaders = false;
private boolean myShowResponseHeaders = true;
/**
* Constructor
*/
public ResponseHighlighterInterceptor() {
super();
}
private String createLinkHref(Map<String, String[]> parameters, String formatValue) {
StringBuilder rawB = new StringBuilder();
for (String next : parameters.keySet()) {
if (Constants.PARAM_FORMAT.equals(next)) {
continue;
}
for (String nextValue : parameters.get(next)) {
if (isBlank(nextValue)) {
continue;
}
if (rawB.length() == 0) {
rawB.append('?');
} else {
rawB.append('&');
}
rawB.append(UrlUtil.escape(next));
rawB.append('=');
rawB.append(UrlUtil.escape(nextValue));
}
}
if (rawB.length() == 0) {
rawB.append('?');
} else {
rawB.append('&');
}
rawB.append(Constants.PARAM_FORMAT).append('=').append(formatValue);
String link = rawB.toString();
return link;
}
private int format(String theResultBody, StringBuilder theTarget, EncodingEnum theEncodingEnum) {
String str = StringEscapeUtils.escapeHtml4(theResultBody);
if (str == null || theEncodingEnum == null) {
return str;
theTarget.append(str);
return 0;
}
StringBuilder b = new StringBuilder();
theTarget.append("<div id=\"line1\">");
if (theEncodingEnum == EncodingEnum.JSON) {
boolean inValue = false;
boolean inQuote = false;
boolean inTag = false;
int lineCount = 1;
for (int i = 0; i < str.length(); i++) {
char prevChar = (i > 0) ? str.charAt(i - 1) : ' ';
char nextChar = str.charAt(i);
char nextChar2 = (i + 1) < str.length() ? str.charAt(i + 1) : ' ';
char nextChar3 = (i + 2) < str.length() ? str.charAt(i + 2) : ' ';
char nextChar4 = (i + 3) < str.length() ? str.charAt(i + 3) : ' ';
char nextChar5 = (i + 4) < str.length() ? str.charAt(i + 4) : ' ';
char nextChar6 = (i + 5) < str.length() ? str.charAt(i + 5) : ' ';
if (nextChar == '\n') {
lineCount++;
theTarget.append("</div><div id=\"line");
theTarget.append(lineCount);
theTarget.append("\" onclick=\"updateHighlightedLineTo('#L");
theTarget.append(lineCount);
theTarget.append("');\">");
continue;
}
if (theEncodingEnum == EncodingEnum.JSON) {
boolean inValue = false;
boolean inQuote = false;
for (int i = 0; i < str.length(); i++) {
char prevChar = (i > 0) ? str.charAt(i - 1) : ' ';
char nextChar = str.charAt(i);
char nextChar2 = (i + 1) < str.length() ? str.charAt(i + 1) : ' ';
char nextChar3 = (i + 2) < str.length() ? str.charAt(i + 2) : ' ';
char nextChar4 = (i + 3) < str.length() ? str.charAt(i + 3) : ' ';
char nextChar5 = (i + 4) < str.length() ? str.charAt(i + 4) : ' ';
char nextChar6 = (i + 5) < str.length() ? str.charAt(i + 5) : ' ';
if (inQuote) {
b.append(nextChar);
theTarget.append(nextChar);
if (prevChar != '\\' && nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
b.append("quot;</span>");
theTarget.append("quot;</span>");
i += 5;
inQuote = false;
} else if (nextChar == '\\' && nextChar2 == '"') {
b.append("quot;</span>");
theTarget.append("quot;</span>");
i += 5;
inQuote = false;
}
} else {
if (nextChar == ':') {
inValue = true;
b.append(nextChar);
theTarget.append(nextChar);
} else if (nextChar == '[' || nextChar == '{') {
b.append("<span class='hlControl'>");
b.append(nextChar);
b.append("</span>");
theTarget.append("<span class='hlControl'>");
theTarget.append(nextChar);
theTarget.append("</span>");
inValue = false;
} else if (nextChar == '{' || nextChar == '}' || nextChar == ',') {
b.append("<span class='hlControl'>");
b.append(nextChar);
b.append("</span>");
theTarget.append("<span class='hlControl'>");
theTarget.append(nextChar);
theTarget.append("</span>");
inValue = false;
} else if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
if (inValue) {
b.append("<span class='hlQuot'>&quot;");
theTarget.append("<span class='hlQuot'>&quot;");
} else {
b.append("<span class='hlTagName'>&quot;");
theTarget.append("<span class='hlTagName'>&quot;");
}
inQuote = true;
i += 5;
} else if (nextChar == ':') {
b.append("<span class='hlControl'>");
b.append(nextChar);
b.append("</span>");
theTarget.append("<span class='hlControl'>");
theTarget.append(nextChar);
theTarget.append("</span>");
inValue = true;
} else {
b.append(nextChar);
theTarget.append(nextChar);
}
}
}
} else {
boolean inQuote = false;
boolean inTag = false;
for (int i = 0; i < str.length(); i++) {
char nextChar = str.charAt(i);
char nextChar2 = (i + 1) < str.length() ? str.charAt(i + 1) : ' ';
char nextChar3 = (i + 2) < str.length() ? str.charAt(i + 2) : ' ';
char nextChar4 = (i + 3) < str.length() ? str.charAt(i + 3) : ' ';
char nextChar5 = (i + 4) < str.length() ? str.charAt(i + 4) : ' ';
char nextChar6 = (i + 5) < str.length() ? str.charAt(i + 5) : ' ';
} else {
if (inQuote) {
b.append(nextChar);
theTarget.append(nextChar);
if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
b.append("quot;</span>");
theTarget.append("quot;</span>");
i += 5;
inQuote = false;
}
} else if (inTag) {
if (nextChar == '&' && nextChar2 == 'g' && nextChar3 == 't' && nextChar4 == ';') {
b.append("</span><span class='hlControl'>&gt;</span>");
theTarget.append("</span><span class='hlControl'>&gt;</span>");
inTag = false;
i += 3;
} else if (nextChar == ' ') {
b.append("</span><span class='hlAttr'>");
b.append(nextChar);
theTarget.append("</span><span class='hlAttr'>");
theTarget.append(nextChar);
} else if (nextChar == '&' && nextChar2 == 'q' && nextChar3 == 'u' && nextChar4 == 'o' && nextChar5 == 't' && nextChar6 == ';') {
b.append("<span class='hlQuot'>&quot;");
theTarget.append("<span class='hlQuot'>&quot;");
inQuote = true;
i += 5;
} else {
b.append(nextChar);
theTarget.append(nextChar);
}
} else {
if (nextChar == '&' && nextChar2 == 'l' && nextChar3 == 't' && nextChar4 == ';') {
b.append("<span class='hlControl'>&lt;</span><span class='hlTagName'>");
theTarget.append("<span class='hlControl'>&lt;</span><span class='hlTagName'>");
inTag = true;
i += 3;
} else {
b.append(nextChar);
theTarget.append(nextChar);
}
}
}
}
return b.toString();
theTarget.append("</div>");
return lineCount;
}
@Override
@ -213,6 +258,22 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
return false;
}
/**
* If set to <code>true</code> (default is <code>false</code>) response will include the
* request headers
*/
public boolean isShowRequestHeaders() {
return myShowRequestHeaders;
}
/**
* If set to <code>true</code> (default is <code>true</code>) response will include the
* response headers
*/
public boolean isShowResponseHeaders() {
return myShowResponseHeaders;
}
@Override
public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject, HttpServletRequest theServletRequest, HttpServletResponse theServletResponse)
throws AuthenticationException {
@ -283,7 +344,56 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
return false;
}
/**
* If set to <code>true</code> (default is <code>false</code>) response will include the
* request headers
*
* @return Returns a reference to this for easy method chaining
*/
public ResponseHighlighterInterceptor setShowRequestHeaders(boolean theShowRequestHeaders) {
myShowRequestHeaders = theShowRequestHeaders;
return this;
}
/**
* If set to <code>true</code> (default is <code>true</code>) response will include the
* response headers
*
* @return Returns a reference to this for easy method chaining
*/
public ResponseHighlighterInterceptor setShowResponseHeaders(boolean theShowResponseHeaders) {
myShowResponseHeaders = theShowResponseHeaders;
return this;
}
private void streamRequestHeaders(ServletRequest theServletRequest, StringBuilder b) {
if (theServletRequest instanceof HttpServletRequest) {
HttpServletRequest sr = (HttpServletRequest) theServletRequest;
b.append("<h1>Request</h1>");
b.append("<div class=\"headersDiv\">");
Enumeration<String> headerNamesEnum = sr.getHeaderNames();
while (headerNamesEnum.hasMoreElements()) {
String nextHeaderName = headerNamesEnum.nextElement();
Enumeration<String> headerValuesEnum = sr.getHeaders(nextHeaderName);
while (headerValuesEnum.hasMoreElements()) {
String nextHeaderValue = headerValuesEnum.nextElement();
b.append("<div class=\"headersRow\">");
b.append("<span class=\"headerName\">").append(nextHeaderName).append(": ").append("</span>");
b.append("<span class=\"headerValue\">").append(nextHeaderValue).append("</span>");
b.append("</div>");
}
}
b.append("</div>");
}
}
private void streamResponse(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, IBaseResource resource, ServletRequest theServletRequest, int theStatusCode) {
if (theRequestDetails.getServer() instanceof RestfulServer) {
RestfulServer rs = (RestfulServer) theRequestDetails.getServer();
rs.addHeadersToResponse(theServletResponse);
}
IParser p;
Map<String, String[]> parameters = theRequestDetails.getParameters();
if (parameters.containsKey(Constants.PARAM_FORMAT)) {
@ -322,8 +432,18 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
b.append(" <head>\n");
b.append(" <meta charset=\"utf-8\" />\n");
b.append(" <style>\n");
b.append(".hlQuot {\n");
b.append(" color: #88F;\n");
b.append(".httpStatusDiv {");
b.append(" font-size: 1.2em;");
b.append(" font-weight: bold;");
b.append("}");
b.append(".hlQuot { color: #88F; }\n");
b.append(".hlQuot a { text-decoration: underline; text-decoration-color: #CCC; }\n");
b.append(".hlQuot a:HOVER { text-decoration: underline; text-decoration-color: #008; }\n");
b.append(".hlQuot .uuid, .hlQuot .dateTime {\n");
b.append(" user-select: all;\n");
b.append(" -moz-user-select: all;\n");
b.append(" -webkit-user-select: all;\n");
b.append(" -ms-user-select: element;\n");
b.append("}\n");
b.append(".hlAttr {\n");
b.append(" color: #888;\n");
@ -340,7 +460,12 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
b.append(".hlUrlBase {\n");
b.append("}");
b.append(".headersDiv {\n");
b.append(" background: #EEE;");
b.append(" padding: 10px;");
b.append(" margin-left: 10px;");
b.append(" border: 1px solid #CCC;");
b.append(" border-radius: 10px;");
b.append("}");
b.append(".headersRow {\n");
b.append("}");
b.append(".headerName {\n");
b.append(" color: #888;\n");
@ -350,6 +475,32 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
b.append(" color: #88F;\n");
b.append(" font-family: monospace;\n");
b.append("}");
b.append(".responseBodyTable {");
b.append(" width: 100%;");
b.append(" margin-left: 0px;");
b.append(" margin-top: 20px;");
b.append("}");
b.append(".responseBodyTableFirstColumn {");
b.append("}");
b.append(".responseBodyTableSecondColumn {");
b.append(" width: 100%;");
b.append("}");
b.append(".lineAnchor A {");
b.append(" text-decoration: none;");
b.append(" padding-left: 20px;");
b.append("}");
b.append(".lineAnchor {");
b.append(" display: block;");
b.append(" padding-right: 20px;");
b.append("}");
b.append(".selectedLine {");
b.append(" background-color: #EEF;");
b.append(" font-weight: bold;");
b.append("}");
b.append("H1 {");
b.append(" font-size: 1.1em;");
b.append(" color: #666;");
b.append("}");
b.append("BODY {\n");
b.append(" font-family: Arial;\n");
b.append("}");
@ -393,20 +544,74 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
b.append("\n");
// if (isEncodeHeaders()) {
// b.append("<h1>Request Headers</h1>");
// b.append("<div class=\"headersDiv\">");
// for (int next : theRequestDetails.get)
// b.append("</div>");
// b.append("<h1>Response Headers</h1>");
// b.append("<div class=\"headersDiv\">");
// b.append("</div>");
// b.append("<h1>Response Body</h1>");
// }
b.append("<pre>");
b.append(format(encoded, encoding));
b.append("</pre>");
b.append(" </body>");
// status (e.g. HTTP 200 OK)
String statusName = Constants.HTTP_STATUS_NAMES.get(theServletResponse.getStatus());
statusName = defaultString(statusName);
b.append("<div class=\"httpStatusDiv\">");
b.append("HTTP ");
b.append(theServletResponse.getStatus());
b.append(" ");
b.append(statusName);
b.append("</div>");
b.append("\n");
b.append("\n");
if (true) {
try {
if (isShowRequestHeaders()) {
streamRequestHeaders(theServletRequest, b);
}
if (isShowResponseHeaders()) {
streamResponseHeaders(theRequestDetails, theServletResponse, b);
}
} catch (Throwable t) {
// ignore (this will hit if we're running in a servlet 2.5 environment)
}
}
StringBuilder target = new StringBuilder();
int linesCount = format(encoded, target, encoding);
b.append("<table class=\"responseBodyTable\" cellspacing=\"0\">");
b.append("<tr>");
b.append("<td class=\"responseBodyTableFirstColumn\"><pre>");
for (int i = 1; i <= linesCount; i++) {
b.append("<div class=\"lineAnchor\" id=\"anchor");
b.append(i);
b.append("\">");
b.append("<a href=\"#L");
b.append(i);
b.append("\" name=\"L");
b.append(i);
b.append("\" id=\"L");
b.append(i);
b.append("\">");
b.append(i);
b.append("</a></div>");
}
b.append("</pre></td>");
// Response Body
b.append("<td class=\"responseBodyTableSecondColumn\"><pre>");
b.append(target);
b.append("</pre></td>");
b.append("</tr>");
b.append("</table>");
b.append("\n");
InputStream jsStream = ResponseHighlighterInterceptor.class.getResourceAsStream("ResponseHighlighter.js");
String jsStr = jsStream != null ? IOUtils.toString(jsStream, "UTF-8") : "console.log('ResponseHighlighterInterceptor: javascript resource not found')";
jsStr = jsStr.replace("FHIR_BASE", theRequestDetails.getServerBaseForRequest());
b.append("<script type=\"text/javascript\">");
b.append(jsStr);
b.append("</script>\n");
b.append("</body>");
b.append("</html>");
//@formatter:off
String out = b.toString();
@ -419,35 +624,31 @@ public class ResponseHighlighterInterceptor extends InterceptorAdapter {
}
}
private String createLinkHref(Map<String, String[]> parameters, String formatValue) {
StringBuilder rawB = new StringBuilder();
for (String next : parameters.keySet()) {
if (Constants.PARAM_FORMAT.equals(next)) {
continue;
}
for (String nextValue : parameters.get(next)) {
if (isBlank(nextValue)) {
continue;
}
if (rawB.length() == 0) {
rawB.append('?');
} else {
rawB.append('&');
}
rawB.append(UrlUtil.escape(next));
rawB.append('=');
rawB.append(UrlUtil.escape(nextValue));
}
}
if (rawB.length() == 0) {
rawB.append('?');
} else {
rawB.append('&');
}
rawB.append(Constants.PARAM_FORMAT).append('=').append(formatValue);
private void streamResponseHeaders(RequestDetails theRequestDetails, HttpServletResponse theServletResponse, StringBuilder b) {
if (theServletResponse.getHeaderNames().isEmpty() == false) {
b.append("<h1>Response</h1>");
String link = rawB.toString();
return link;
b.append("<div class=\"headersDiv\">");
for (String nextHeaderName : theServletResponse.getHeaderNames()) {
for (String nextHeaderValue : theServletResponse.getHeaders(nextHeaderName)) {
/*
* Let's pretend we're returning a FHIR content type even though we're
* actually returning an HTML one
*/
if (nextHeaderName.equalsIgnoreCase(Constants.HEADER_CONTENT_TYPE)) {
ResponseEncoding responseEncoding = RestfulServerUtils.determineResponseEncodingNoDefault(theRequestDetails, theRequestDetails.getServer().getDefaultResponseEncoding());
if (responseEncoding != null && isNotBlank(responseEncoding.getResourceContentType())) {
nextHeaderValue = responseEncoding.getResourceContentType() + ";charset=utf-8";
}
}
b.append("<div class=\"headersRow\">");
b.append("<span class=\"headerName\">").append(nextHeaderName).append(": ").append("</span>");
b.append("<span class=\"headerValue\">").append(nextHeaderValue).append("</span>");
b.append("</div>");
}
}
b.append("</div>");
}
}
}

View File

@ -154,6 +154,7 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter
case CREATE:
case UPDATE:
case PATCH:
// if (theRequestResource != null) {
// if (theRequestResource.getIdElement() != null) {
// if (theRequestResource.getIdElement().hasIdPart() == false) {

View File

@ -42,14 +42,13 @@
<optional>true</optional>
</dependency>
<!-- The JPA project uses a newer API but we'll try to hold to this version as much as possible. See #283. -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<!-- <dependency> <groupId>ca.uhn.hapi.fhir</groupId> <artifactId>hapi-fhir-structures-dstu</artifactId> <version>0.8</version> <scope>test</scope> </dependency> -->
<dependency>

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.rest.server.interceptor;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.containsStringIgnoringCase;
import static org.hamcrest.Matchers.matchesPattern;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.stringContainsInOrder;
@ -32,8 +33,7 @@ import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.*;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
@ -53,14 +53,14 @@ import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.UriDt;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.*;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.*;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.util.*;
public class ResponseHighlightingInterceptorTest {
private static ResponseHighlighterInterceptor ourInterceptor = new ResponseHighlighterInterceptor();
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forDstu2();
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ResponseHighlightingInterceptorTest.class);
@ -74,6 +74,12 @@ public class ResponseHighlightingInterceptorTest {
TestUtil.clearAllStaticFieldsForUnitTest();
}
@Before
public void before() {
ourInterceptor.setShowRequestHeaders(new ResponseHighlighterInterceptor().isShowRequestHeaders());
ourInterceptor.setShowResponseHeaders(new ResponseHighlighterInterceptor().isShowResponseHeaders());
}
/**
* See #464
*/
@ -89,7 +95,7 @@ public class ResponseHighlightingInterceptorTest {
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(responseContent, (stringContainsInOrder("<body>", "<pre>", "\n", "</pre>")));
assertThat(responseContent, (stringContainsInOrder("<body>", "<pre>", "<div", "</pre>")));
}
/**
@ -107,7 +113,7 @@ public class ResponseHighlightingInterceptorTest {
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertThat(responseContent, (stringContainsInOrder("<body>", "<pre>", "\n", "</pre>")));
assertThat(responseContent, (stringContainsInOrder("<body>", "<pre>", "<div", "</pre>")));
}
/**
@ -142,6 +148,73 @@ public class ResponseHighlightingInterceptorTest {
}
@Test
public void testShowNeither() throws Exception {
ourInterceptor.setShowRequestHeaders(false);
ourInterceptor.setShowResponseHeaders(false);
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/json");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase());
assertThat(responseContent, not(containsStringIgnoringCase("Accept")));
assertThat(responseContent, not(containsStringIgnoringCase("Content-Type")));
}
@Test
public void testShowResponse() throws Exception {
ourInterceptor.setShowResponseHeaders(true);
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/json");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase());
assertThat(responseContent, not(containsStringIgnoringCase("Accept")));
assertThat(responseContent, (containsStringIgnoringCase("Content-Type")));
}
@Test
public void testShowRequest() throws Exception {
ourInterceptor.setShowRequestHeaders(true);
ourInterceptor.setShowResponseHeaders(false);
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/json");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase());
assertThat(responseContent, (containsStringIgnoringCase("Accept")));
assertThat(responseContent, not(containsStringIgnoringCase("Content-Type")));
}
@Test
public void testShowRequestAndResponse() throws Exception {
ourInterceptor.setShowRequestHeaders(true);
ourInterceptor.setShowResponseHeaders(true);
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1?_format=html/json");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info(responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals("text/html;charset=utf-8", status.getFirstHeader("content-type").getValue().replace(" ", "").toLowerCase());
assertThat(responseContent, (containsStringIgnoringCase("Accept")));
assertThat(responseContent, (containsStringIgnoringCase("Content-Type")));
}
@Test
public void testGetInvalidResource() throws Exception {
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Foobar/123");
@ -190,7 +263,7 @@ public class ResponseHighlightingInterceptorTest {
@Test
public void testHighlightException() throws Exception {
ResponseHighlighterInterceptor ic = new ResponseHighlighterInterceptor();
ResponseHighlighterInterceptor ic = ourInterceptor;
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer<Enumeration<String>>() {
@ -207,7 +280,7 @@ public class ResponseHighlightingInterceptorTest {
Patient resource = new Patient();
resource.addName().addFamily("FAMILY");
ServletRequestDetails reqDetails = new ServletRequestDetails();
ServletRequestDetails reqDetails = new TestServletRequestDetails();
reqDetails.setRequestType(RequestTypeEnum.GET);
reqDetails.setServer(new RestfulServer(ourCtx));
reqDetails.setServletRequest(req);
@ -228,7 +301,7 @@ public class ResponseHighlightingInterceptorTest {
@Test
public void testHighlightNormalResponseForcePrettyPrint() throws Exception {
ResponseHighlighterInterceptor ic = new ResponseHighlighterInterceptor();
ResponseHighlighterInterceptor ic = ourInterceptor;
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer<Enumeration<String>>() {
@ -245,7 +318,7 @@ public class ResponseHighlightingInterceptorTest {
Patient resource = new Patient();
resource.addName().addFamily("FAMILY");
ServletRequestDetails reqDetails = new ServletRequestDetails();
ServletRequestDetails reqDetails = new TestServletRequestDetails();
reqDetails.setRequestType(RequestTypeEnum.GET);
HashMap<String, String[]> params = new HashMap<String, String[]>();
params.put(Constants.PARAM_PRETTY, new String[] { Constants.PARAM_PRETTY_VALUE_TRUE });
@ -258,12 +331,12 @@ public class ResponseHighlightingInterceptorTest {
String output = sw.getBuffer().toString();
ourLog.info(output);
assertThat(output, containsString("<span class='hlTagName'>Patient</span>"));
assertThat(output, stringContainsInOrder("<body>", "<pre>", "\n", "</pre>"));
assertThat(output, stringContainsInOrder("<body>", "<pre>", "<div", "</pre>"));
}
@Test
public void testHighlightForceRaw() throws Exception {
ResponseHighlighterInterceptor ic = new ResponseHighlighterInterceptor();
ResponseHighlighterInterceptor ic = ourInterceptor;
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer<Enumeration<String>>() {
@ -280,7 +353,7 @@ public class ResponseHighlightingInterceptorTest {
Patient resource = new Patient();
resource.addName().addFamily("FAMILY");
ServletRequestDetails reqDetails = new ServletRequestDetails();
ServletRequestDetails reqDetails = new TestServletRequestDetails();
reqDetails.setRequestType(RequestTypeEnum.GET);
HashMap<String, String[]> params = new HashMap<String, String[]>();
params.put(Constants.PARAM_PRETTY, new String[] { Constants.PARAM_PRETTY_VALUE_TRUE });
@ -297,7 +370,7 @@ public class ResponseHighlightingInterceptorTest {
@Test
public void testDontHighlightWhenOriginHeaderPresent() throws Exception {
ResponseHighlighterInterceptor ic = new ResponseHighlighterInterceptor();
ResponseHighlighterInterceptor ic = ourInterceptor;
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer<Enumeration<String>>() {
@ -320,7 +393,7 @@ public class ResponseHighlightingInterceptorTest {
Patient resource = new Patient();
resource.addName().addFamily("FAMILY");
ServletRequestDetails reqDetails = new ServletRequestDetails();
ServletRequestDetails reqDetails = new TestServletRequestDetails();
reqDetails.setRequestType(RequestTypeEnum.GET);
HashMap<String, String[]> params = new HashMap<String, String[]>();
reqDetails.setParameters(params);
@ -337,7 +410,7 @@ public class ResponseHighlightingInterceptorTest {
*/
@Test
public void testHighlightForceHtmlCt() throws Exception {
ResponseHighlighterInterceptor ic = new ResponseHighlighterInterceptor();
ResponseHighlighterInterceptor ic = ourInterceptor;
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer<Enumeration<String>>() {
@ -354,7 +427,7 @@ public class ResponseHighlightingInterceptorTest {
Patient resource = new Patient();
resource.addName().addFamily("FAMILY");
ServletRequestDetails reqDetails = new ServletRequestDetails();
ServletRequestDetails reqDetails = new TestServletRequestDetails();
reqDetails.setRequestType(RequestTypeEnum.GET);
HashMap<String, String[]> params = new HashMap<String, String[]>();
params.put(Constants.PARAM_FORMAT, new String[] { Constants.FORMAT_HTML });
@ -371,7 +444,7 @@ public class ResponseHighlightingInterceptorTest {
*/
@Test
public void testHighlightForceHtmlFormat() throws Exception {
ResponseHighlighterInterceptor ic = new ResponseHighlighterInterceptor();
ResponseHighlighterInterceptor ic = ourInterceptor;
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer<Enumeration<String>>() {
@ -388,7 +461,7 @@ public class ResponseHighlightingInterceptorTest {
Patient resource = new Patient();
resource.addName().addFamily("FAMILY");
ServletRequestDetails reqDetails = new ServletRequestDetails();
ServletRequestDetails reqDetails = new TestServletRequestDetails();
reqDetails.setRequestType(RequestTypeEnum.GET);
HashMap<String, String[]> params = new HashMap<String, String[]>();
params.put(Constants.PARAM_FORMAT, new String[] { Constants.CT_HTML });
@ -402,7 +475,7 @@ public class ResponseHighlightingInterceptorTest {
@Test
public void testHighlightNormalResponse() throws Exception {
ResponseHighlighterInterceptor ic = new ResponseHighlighterInterceptor();
ResponseHighlighterInterceptor ic = ourInterceptor;
HttpServletRequest req = mock(HttpServletRequest.class);
when(req.getHeaders(Constants.HEADER_ACCEPT)).thenAnswer(new Answer<Enumeration<String>>() {
@ -419,7 +492,7 @@ public class ResponseHighlightingInterceptorTest {
Patient resource = new Patient();
resource.addName().addFamily("FAMILY");
ServletRequestDetails reqDetails = new ServletRequestDetails();
ServletRequestDetails reqDetails = new TestServletRequestDetails();
reqDetails.setRequestType(RequestTypeEnum.GET);
reqDetails.setParameters(new HashMap<String, String[]>());
reqDetails.setServer(new RestfulServer(ourCtx));
@ -430,7 +503,7 @@ public class ResponseHighlightingInterceptorTest {
String output = sw.getBuffer().toString();
ourLog.info(output);
assertThat(output, containsString("<span class='hlTagName'>Patient</span>"));
assertThat(output, stringContainsInOrder("<body>", "<pre>", "\n", "</pre>"));
assertThat(output, stringContainsInOrder("<body>", "<pre>", "<div", "</pre>"));
assertThat(output, containsString("<a href=\"?_format=json\">"));
}
@ -439,7 +512,7 @@ public class ResponseHighlightingInterceptorTest {
*/
@Test
public void testHighlightProducesDefaultJsonWithBrowserRequest() throws Exception {
ResponseHighlighterInterceptor ic = new ResponseHighlighterInterceptor();
ResponseHighlighterInterceptor ic = ourInterceptor;
HttpServletRequest req = mock(HttpServletRequest.class);
@ -457,7 +530,7 @@ public class ResponseHighlightingInterceptorTest {
Patient resource = new Patient();
resource.addName().addFamily("FAMILY");
ServletRequestDetails reqDetails = new ServletRequestDetails();
ServletRequestDetails reqDetails = new TestServletRequestDetails();
reqDetails.setRequestType(RequestTypeEnum.GET);
reqDetails.setParameters(new HashMap<String, String[]>());
RestfulServer server = new RestfulServer(ourCtx);
@ -477,7 +550,7 @@ public class ResponseHighlightingInterceptorTest {
@Test
public void testHighlightProducesDefaultJsonWithBrowserRequest2() throws Exception {
ResponseHighlighterInterceptor ic = new ResponseHighlighterInterceptor();
ResponseHighlighterInterceptor ic = ourInterceptor;
HttpServletRequest req = mock(HttpServletRequest.class);
@ -495,7 +568,7 @@ public class ResponseHighlightingInterceptorTest {
Patient resource = new Patient();
resource.addName().addFamily("FAMILY");
ServletRequestDetails reqDetails = new ServletRequestDetails();
ServletRequestDetails reqDetails = new TestServletRequestDetails();
reqDetails.setRequestType(RequestTypeEnum.GET);
reqDetails.setParameters(new HashMap<String, String[]>());
RestfulServer server = new RestfulServer(ourCtx);
@ -712,7 +785,7 @@ public class ResponseHighlightingInterceptorTest {
config.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS"));
ourServlet.registerInterceptor(corsInterceptor);
ourServlet.registerInterceptor(new ResponseHighlighterInterceptor());
ourServlet.registerInterceptor(ourInterceptor);
ourServlet.setResourceProviders(patientProvider, new DummyBinaryResourceProvider());
ourServlet.setBundleInclusionRule(BundleInclusionRule.BASED_ON_RESOURCE_PRESENCE);
ServletHolder servletHolder = new ServletHolder(ourServlet);
@ -756,9 +829,6 @@ public class ResponseHighlightingInterceptorTest {
}
/**
* Created by dsotnikov on 2/25/2014.
*/
public static class DummyPatientResourceProvider implements IResourceProvider {
private Patient createPatient1() {
@ -861,4 +931,11 @@ public class ResponseHighlightingInterceptorTest {
}
class TestServletRequestDetails extends ServletRequestDetails {
@Override
public String getServerBaseForRequest() {
return "/baseDstu3";
}
}
}

View File

@ -105,7 +105,7 @@ public class AuthorizationInterceptorDstu2Test {
retVal.addName().addFamily("FAM");
return retVal;
}
private IResource createPatient(Integer theId, int theVersion) {
IResource retVal = createPatient(theId);
retVal.setId(retVal.getId().withVersion(Integer.toString(theVersion)));
@ -512,7 +512,7 @@ public class AuthorizationInterceptorDstu2Test {
HttpResponse status;
ourReturn = Arrays.asList(createPatient(2, 1));
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/_history");
status = ourClient.execute(httpGet);
@ -791,8 +791,8 @@ public class AuthorizationInterceptorDstu2Test {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onAnyInstance().andThen()
.build();
.allow("RULE 1").operation().named("opName").onAnyInstance().andThen()
.build();
}
});
@ -1101,7 +1101,7 @@ public class AuthorizationInterceptorDstu2Test {
assertEquals(403, status.getStatusLine().getStatusCode());
assertFalse(ourHitMethod);
}
@Test
public void testReadByAnyId() throws Exception {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@ -1216,8 +1216,8 @@ public class AuthorizationInterceptorDstu2Test {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow("Rule 1").read().resourcesOfType(Patient.class).inCompartment("Patient", new IdDt("Patient/1"))
.build();
.allow("Rule 1").read().resourcesOfType(Patient.class).inCompartment("Patient", new IdDt("Patient/1"))
.build();
}
});
@ -1230,7 +1230,7 @@ public class AuthorizationInterceptorDstu2Test {
for (int i = 0; i < 10; i++) {
ourReturn.add(createPatient(1));
}
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_count=5&_format=json");
status = ourClient.execute(httpGet);
@ -1242,9 +1242,9 @@ public class AuthorizationInterceptorDstu2Test {
assertEquals(10, respBundle.getTotal().intValue());
assertEquals("Patient/1", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
assertNotNull(respBundle.getLink("next"));
// Load next page
ourHitMethod = false;
httpGet = new HttpGet(respBundle.getLink("next").getUrl());
status = ourClient.execute(httpGet);
@ -1265,8 +1265,8 @@ public class AuthorizationInterceptorDstu2Test {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow("Rule 1").read().resourcesOfType(Patient.class).inCompartment("Patient", new IdDt("Patient/1"))
.build();
.allow("Rule 1").read().resourcesOfType(Patient.class).inCompartment("Patient", new IdDt("Patient/1"))
.build();
}
});
@ -1282,7 +1282,7 @@ public class AuthorizationInterceptorDstu2Test {
for (int i = 0; i < 5; i++) {
ourReturn.add(createPatient(2));
}
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_count=5&_format=json");
status = ourClient.execute(httpGet);
@ -1294,9 +1294,9 @@ public class AuthorizationInterceptorDstu2Test {
assertEquals(10, respBundle.getTotal().intValue());
assertEquals("Patient/1", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
assertNotNull(respBundle.getLink("next"));
// Load next page
ourHitMethod = false;
httpGet = new HttpGet(respBundle.getLink("next").getUrl());
status = ourClient.execute(httpGet);
@ -1781,7 +1781,7 @@ public class AuthorizationInterceptorDstu2Test {
@Test
public void testInvalidInstanceIds() throws Exception {
try {
new RuleBuilder().allow("Rule 1").write().instance((String)null);
new RuleBuilder().allow("Rule 1").write().instance((String) null);
fail();
} catch (NullPointerException e) {
assertEquals("theId must not be null or empty", e.getMessage());
@ -1811,14 +1811,51 @@ public class AuthorizationInterceptorDstu2Test {
assertEquals("theId.getValue() must not be null or empty", e.getMessage());
}
try {
new RuleBuilder().allow("Rule 1").write().instance(new IdDt("Observation", (String)null));
new RuleBuilder().allow("Rule 1").write().instance(new IdDt("Observation", (String) null));
fail();
} catch (NullPointerException e) {
assertEquals("theId must contain an ID part", e.getMessage());
}
}
@Test
public void testWritePatchByInstance() throws Exception {
ourConditionalCreateId = "1";
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
//@formatter:off
return new RuleBuilder()
.allow("Rule 1").write().instance("Patient/900").andThen()
.build();
//@formatter:on
}
});
HttpEntityEnclosingRequestBase httpPost;
HttpResponse status;
String response;
String input = "[ { \"op\": \"replace\", \"path\": \"/gender\", \"value\": \"male\" } ]";
ourHitMethod = false;
httpPost = new HttpPatch("http://localhost:" + ourPort + "/Patient/900");
httpPost.setEntity(new StringEntity(input, ContentType.parse("application/json-patch+json")));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
assertEquals(204, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourHitMethod = false;
httpPost = new HttpPatch("http://localhost:" + ourPort + "/Patient/999");
httpPost.setEntity(new StringEntity(input, ContentType.parse("application/json-patch+json")));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
assertEquals(403, status.getStatusLine().getStatusCode());
assertFalse(ourHitMethod);
}
@Test
public void testWriteByInstance() throws Exception {
ourConditionalCreateId = "1";
@ -1874,8 +1911,7 @@ public class AuthorizationInterceptorDstu2Test {
assertFalse(ourHitMethod);
}
@Test
public void testReadByInstance() throws Exception {
ourConditionalCreateId = "1";
@ -1923,7 +1959,6 @@ public class AuthorizationInterceptorDstu2Test {
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
@ -2110,7 +2145,6 @@ public class AuthorizationInterceptorDstu2Test {
return retVal;
}
@Override
public Class<? extends IResource> getResourceType() {
return Patient.class;
@ -2184,6 +2218,14 @@ public class AuthorizationInterceptorDstu2Test {
return retVal;
}
@Patch()
public MethodOutcome patch(@IdParam IdDt theId, @ResourceParam String theResource, PatchTypeEnum thePatchType) {
ourHitMethod = true;
MethodOutcome retVal = new MethodOutcome();
return retVal;
}
@Validate
public MethodOutcome validate(@ResourceParam Patient theResource, @IdParam IdDt theId, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding,
@Validate.Mode ValidationModeEnum theMode, @Validate.Profile String theProfile, RequestDetails theRequestDetails) {
@ -2226,5 +2268,4 @@ public class AuthorizationInterceptorDstu2Test {
}
}

View File

@ -7,6 +7,7 @@ import java.io.InputStreamReader;
import java.util.*;
import org.apache.commons.io.Charsets;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
@ -22,6 +23,10 @@ import ca.uhn.fhir.context.FhirContext;
public class DefaultProfileValidationSupport implements IValidationSupport {
private static final String URL_PREFIX_VALUE_SET = "http://hl7.org/fhir/ValueSet/";
private static final String URL_PREFIX_STRUCTURE_DEFINITION = "http://hl7.org/fhir/StructureDefinition/";
private static final String URL_PREFIX_STRUCTURE_DEFINITION_BASE = "http://hl7.org/fhir/";
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(DefaultProfileValidationSupport.class);
private Map<String, CodeSystem> myCodeSystems;
@ -86,51 +91,28 @@ public class DefaultProfileValidationSupport implements IValidationSupport {
public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {
Validate.notBlank(theUri, "theUri must not be null or blank");
if (theUri.startsWith("http://hl7.org/fhir/StructureDefinition/")) {
if (theClass.equals(StructureDefinition.class)) {
return (T) fetchStructureDefinition(theContext, theUri);
}
if (theUri.startsWith("http://hl7.org/fhir/ValueSet/")) {
if (theClass.equals(ValueSet.class) || theUri.startsWith(URL_PREFIX_VALUE_SET)) {
return (T) fetchValueSet(theContext, theUri);
}
// if (theUri.startsWith("http://hl7.org/fhir/ValueSet/")) {
// Map<String, ValueSet> defaultValueSets = myDefaultValueSets;
// if (defaultValueSets == null) {
// String path = theContext.getVersion().getPathToSchemaDefinitions().replace("/schema", "/valueset") + "/valuesets.xml";
// InputStream valuesetText = DefaultProfileValidationSupport.class.getResourceAsStream(path);
// if (valuesetText == null) {
// return null;
// }
// InputStreamReader reader;
// try {
// reader = new InputStreamReader(valuesetText, "UTF-8");
// } catch (UnsupportedEncodingException e) {
// // Shouldn't happen!
// throw new InternalErrorException("UTF-8 encoding not supported on this platform", e);
// }
//
// defaultValueSets = new HashMap<String, ValueSet>();
//
// Bundle bundle = theContext.newXmlParser().parseResource(Bundle.class, reader);
// for (BundleEntryComponent next : bundle.getEntry()) {
// IdType nextId = new IdType(next.getFullUrl());
// if (nextId.isEmpty() || !nextId.getValue().startsWith("http://hl7.org/fhir/ValueSet/")) {
// continue;
// }
// defaultValueSets.put(nextId.toVersionless().getValue(), (ValueSet) next.getResource());
// }
//
// myDefaultValueSets = defaultValueSets;
// }
//
// return (T) defaultValueSets.get(theUri);
// }
return null;
}
@Override
public StructureDefinition fetchStructureDefinition(FhirContext theContext, String theUrl) {
return provideStructureDefinitionMap(theContext).get(theUrl);
String url = theUrl;
if (url.startsWith(URL_PREFIX_STRUCTURE_DEFINITION)) {
// no change
} else if (url.indexOf('/') == -1) {
url = URL_PREFIX_STRUCTURE_DEFINITION + url;
} else if (StringUtils.countMatches(url, '/') == 1) {
url = URL_PREFIX_STRUCTURE_DEFINITION_BASE + url;
}
return provideStructureDefinitionMap(theContext).get(url);
}
ValueSet fetchValueSet(FhirContext theContext, String theSystem) {

View File

@ -1,14 +1,14 @@
package org.hl7.fhir.dstu3.hapi.validation;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.dstu3.model.CodeSystem;
import org.hl7.fhir.dstu3.model.StructureDefinition;
import org.hl7.fhir.dstu3.model.ValueSet;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.ValueSet.ConceptSetComponent;
import org.hl7.fhir.dstu3.model.ValueSet.ValueSetExpansionComponent;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -21,112 +21,154 @@ import ca.uhn.fhir.context.FhirContext;
*/
public class PrePopulatedValidationSupport implements IValidationSupport {
private Map<String, StructureDefinition> myStructureDefinitions;
private Map<String, ValueSet> myValueSets;
private Map<String, CodeSystem> myCodeSystems;
private Map<String, CodeSystem> myCodeSystems;
private Map<String, StructureDefinition> myStructureDefinitions;
private Map<String, ValueSet> myValueSets;
/**
* Constructor
*/
public PrePopulatedValidationSupport() {
myStructureDefinitions = new HashMap<String,StructureDefinition>();
myValueSets = new HashMap<String,ValueSet>();
myCodeSystems = new HashMap<String,CodeSystem>();
}
/**
* Add a new StructureDefinition resource which will be available to the validator. Note that
* {@link StructureDefinition#getUrl() the URL field) in this resource must contain a value as this
* value will be used as the logical URL.
*/
public void addStructureDefinition(StructureDefinition theStructureDefinition) {
Validate.notBlank(theStructureDefinition.getUrl(), "theStructureDefinition.getUrl() must not return a value");
myStructureDefinitions.put(theStructureDefinition.getUrl(), theStructureDefinition);
}
/**
* Add a new ValueSet resource which will be available to the validator. Note that
* {@link ValueSet#getUrl() the URL field) in this resource must contain a value as this
* value will be used as the logical URL.
*/
public void addValueSet(ValueSet theValueSet) {
Validate.notBlank(theValueSet.getUrl(), "theValueSet.getUrl() must not return a value");
myValueSets.put(theValueSet.getUrl(), theValueSet);
}
/**
* Constructor
*/
public PrePopulatedValidationSupport() {
myStructureDefinitions = new HashMap<String, StructureDefinition>();
myValueSets = new HashMap<String, ValueSet>();
myCodeSystems = new HashMap<String, CodeSystem>();
}
/**
* Add a new CodeSystem resource which will be available to the validator. Note that
* {@link CodeSystem#getUrl() the URL field) in this resource must contain a value as this
* value will be used as the logical URL.
*/
public void addCodeSystem(CodeSystem theCodeSystem) {
Validate.notBlank(theCodeSystem.getUrl(), "theCodeSystem.getUrl() must not return a value");
myCodeSystems.put(theCodeSystem.getUrl(), theCodeSystem);
}
/**
* Constructor
*
* @param theStructureDefinitions
* The StructureDefinitions to be returned by this module. Keys are the logical URL for the resource, and
* values are the resource itself.
* @param theValueSets
* The ValueSets to be returned by this module. Keys are the logical URL for the resource, and values are
* the resource itself.
* @param theCodeSystems
* The CodeSystems to be returned by this module. Keys are the logical URL for the resource, and values are
* the resource itself.
*/
public PrePopulatedValidationSupport(Map<String, StructureDefinition> theStructureDefinitions, Map<String, ValueSet> theValueSets, Map<String, CodeSystem> theCodeSystems) {
myStructureDefinitions = theStructureDefinitions;
myValueSets = theValueSets;
myCodeSystems = theCodeSystems;
}
/**
* Constructor
*
* @param theStructureDefinitions
* The StructureDefinitions to be returned by this module. Keys are the logical URL for the resource, and
* values are the resource itself.
* @param theValueSets
* The ValueSets to be returned by this module. Keys are the logical URL for the resource, and values are
* the resource itself.
* @param theCodeSystems
* The CodeSystems to be returned by this module. Keys are the logical URL for the resource, and values are
* the resource itself.
*/
public PrePopulatedValidationSupport(Map<String, StructureDefinition> theStructureDefinitions, Map<String, ValueSet> theValueSets, Map<String, CodeSystem> theCodeSystems) {
myStructureDefinitions = theStructureDefinitions;
myValueSets = theValueSets;
myCodeSystems = theCodeSystems;
}
/**
* Add a new CodeSystem resource which will be available to the validator. Note that
* {@link CodeSystem#getUrl() the URL field) in this resource must contain a value as this
* value will be used as the logical URL.
* <p>
* Note that if the URL is a canonical FHIR URL (e.g. http://hl7.org/StructureDefinition/Extension),
* it will be stored in three ways:
* <ul>
* <li>Extension</li>
* <li>StructureDefinition/Extension</li>
* <li>http://hl7.org/StructureDefinition/Extension</li>
* </ul>
* </p>
*/
public void addCodeSystem(CodeSystem theCodeSystem) {
Validate.notBlank(theCodeSystem.getUrl(), "theCodeSystem.getUrl() must not return a value");
addToMap(theCodeSystem, myCodeSystems, theCodeSystem.getUrl());
}
@Override
public ValueSetExpansionComponent expandValueSet(FhirContext theContext, ConceptSetComponent theInclude) {
return null;
}
/**
* Add a new StructureDefinition resource which will be available to the validator. Note that
* {@link StructureDefinition#getUrl() the URL field) in this resource must contain a value as this
* value will be used as the logical URL.
* <p>
* Note that if the URL is a canonical FHIR URL (e.g. http://hl7.org/StructureDefinition/Extension),
* it will be stored in three ways:
* <ul>
* <li>Extension</li>
* <li>StructureDefinition/Extension</li>
* <li>http://hl7.org/StructureDefinition/Extension</li>
* </ul>
* </p>
*/
public void addStructureDefinition(StructureDefinition theStructureDefinition) {
Validate.notBlank(theStructureDefinition.getUrl(), "theStructureDefinition.getUrl() must not return a value");
addToMap(theStructureDefinition, myStructureDefinitions, theStructureDefinition.getUrl());
}
@Override
public List<StructureDefinition> fetchAllStructureDefinitions(FhirContext theContext) {
return new ArrayList<StructureDefinition>(myStructureDefinitions.values());
}
private <T extends MetadataResource> void addToMap(T theStructureDefinition, Map<String, T> map, String theUrl) {
if (isNotBlank(theUrl)) {
map.put(theUrl, theStructureDefinition);
@Override
public CodeSystem fetchCodeSystem(FhirContext theContext, String theSystem) {
return myCodeSystems.get(theSystem);
}
int lastSlashIdx = theUrl.lastIndexOf('/');
if (lastSlashIdx != -1) {
map.put(theUrl.substring(lastSlashIdx + 1), theStructureDefinition);
int previousSlashIdx = theUrl.lastIndexOf('/', lastSlashIdx - 1);
if (previousSlashIdx != -1) {
map.put(theUrl.substring(previousSlashIdx + 1), theStructureDefinition);
}
}
@SuppressWarnings("unchecked")
@Override
public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {
if (theClass.equals(StructureDefinition.class)) {
return (T) myStructureDefinitions.get(theUri);
}
if (theClass.equals(ValueSet.class)) {
return (T) myValueSets.get(theUri);
}
if (theClass.equals(CodeSystem.class)) {
return (T) myCodeSystems.get(theUri);
}
return null;
}
}
}
@Override
public StructureDefinition fetchStructureDefinition(FhirContext theCtx, String theUrl) {
return myStructureDefinitions.get(theUrl);
}
/**
* Add a new ValueSet resource which will be available to the validator. Note that
* {@link ValueSet#getUrl() the URL field) in this resource must contain a value as this
* value will be used as the logical URL.
* <p>
* Note that if the URL is a canonical FHIR URL (e.g. http://hl7.org/StructureDefinition/Extension),
* it will be stored in three ways:
* <ul>
* <li>Extension</li>
* <li>StructureDefinition/Extension</li>
* <li>http://hl7.org/StructureDefinition/Extension</li>
* </ul>
* </p>
*/
public void addValueSet(ValueSet theValueSet) {
Validate.notBlank(theValueSet.getUrl(), "theValueSet.getUrl() must not return a value");
addToMap(theValueSet, myValueSets, theValueSet.getUrl());
}
@Override
public boolean isCodeSystemSupported(FhirContext theContext, String theSystem) {
return false;
}
@Override
public ValueSetExpansionComponent expandValueSet(FhirContext theContext, ConceptSetComponent theInclude) {
return null;
}
@Override
public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) {
return null;
}
@Override
public List<StructureDefinition> fetchAllStructureDefinitions(FhirContext theContext) {
return new ArrayList<StructureDefinition>(myStructureDefinitions.values());
}
@Override
public CodeSystem fetchCodeSystem(FhirContext theContext, String theSystem) {
return myCodeSystems.get(theSystem);
}
@SuppressWarnings("unchecked")
@Override
public <T extends IBaseResource> T fetchResource(FhirContext theContext, Class<T> theClass, String theUri) {
if (theClass.equals(StructureDefinition.class)) {
return (T) myStructureDefinitions.get(theUri);
}
if (theClass.equals(ValueSet.class)) {
return (T) myValueSets.get(theUri);
}
if (theClass.equals(CodeSystem.class)) {
return (T) myCodeSystems.get(theUri);
}
return null;
}
@Override
public StructureDefinition fetchStructureDefinition(FhirContext theCtx, String theUrl) {
return myStructureDefinitions.get(theUrl);
}
@Override
public boolean isCodeSystemSupported(FhirContext theContext, String theSystem) {
return false;
}
@Override
public CodeValidationResult validateCode(FhirContext theContext, String theCodeSystem, String theCode, String theDisplay) {
return null;
}
}

View File

@ -0,0 +1,34 @@
package org.hl7.fhir.dstu3.hapi.validation;
import static org.junit.Assert.*;
import org.junit.AfterClass;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.util.TestUtil;
public class DefaultProfileValidationSupportTest {
private DefaultProfileValidationSupport mySvc = new DefaultProfileValidationSupport();
private static FhirContext ourCtx = FhirContext.forDstu3();
@Test
public void testGetStructureDefinitionsWithRelativeUrls() {
assertNotNull(mySvc.fetchStructureDefinition(ourCtx, "http://hl7.org/fhir/StructureDefinition/Extension"));
assertNotNull(mySvc.fetchStructureDefinition(ourCtx, "StructureDefinition/Extension"));
assertNotNull(mySvc.fetchStructureDefinition(ourCtx, "Extension"));
assertNull(mySvc.fetchStructureDefinition(ourCtx, "http://hl7.org/fhir/StructureDefinition/Extension2"));
assertNull(mySvc.fetchStructureDefinition(ourCtx, "StructureDefinition/Extension2"));
assertNull(mySvc.fetchStructureDefinition(ourCtx, "Extension2"));
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -0,0 +1,322 @@
package org.hl7.fhir.dstu3.hapi.validation;
import static org.junit.Assert.assertEquals;
import java.util.*;
import javax.annotation.Nullable;
import org.hl7.fhir.dstu3.context.IWorkerContext;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.utils.StructureMapUtilities;
import org.junit.Before;
import org.junit.Test;
import ca.uhn.fhir.context.FhirContext;
public class StructureMapTest {
/**
* The logger object.
*/
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(StructureMapTest.class);
/**
* The basic fhir context used to parse structure definitions.
*/
FhirContext context;
/**
* HapiFhirContext used when building strucutre map utilities.
*/
IWorkerContext hapiContext;
/**
* path to the files used to test the profile generator.
*/
String resourcePath = null;
/**
* Used to validate definitions as well as add new structure definitions to a registry.
*/
PrePopulatedValidationSupport validationSupport;
public StructureMap.StructureMapGroupRuleSourceComponent buildSource(String context, @Nullable String element, @Nullable String variable, @Nullable String type, @Nullable Integer min,
@Nullable String max) {
StructureMap.StructureMapGroupRuleSourceComponent retVal = new StructureMap.StructureMapGroupRuleSourceComponent();
retVal.setContext(context);
if (element != null)
retVal.setElement(element);
if (variable != null)
retVal.setVariable(variable);
if (type != null)
retVal.setType(type);
if (min != null)
retVal.setMin(min);
if (max != null)
retVal.setMax(max);
return retVal;
}
public List<StructureMap.StructureMapGroupRuleSourceComponent> buildSourceList(StructureMap.StructureMapGroupRuleSourceComponent[] sources) {
List<StructureMap.StructureMapGroupRuleSourceComponent> retVal = new ArrayList<StructureMap.StructureMapGroupRuleSourceComponent>();
retVal.addAll(Arrays.asList(sources));
return retVal;
}
public StructureMap.StructureMapGroupRuleTargetComponent buildTarget(@Nullable String context, @Nullable String element, @Nullable String variable,
@Nullable StructureMap.StructureMapTransform transform, @Nullable TargetParam[] params) throws Exception {
StructureMap.StructureMapGroupRuleTargetComponent retVal = new StructureMap.StructureMapGroupRuleTargetComponent();
if (context != null)
retVal.setContext(context);
if (element != null)
retVal.setElement(element);
if (variable != null)
retVal.setVariable(variable);
if (transform != null)
retVal.setTransform(transform);
if (params != null) {
if (params.length > 0)
retVal.setParameter(this.constructParameters(params));
}
return retVal;
}
public List<StructureMap.StructureMapGroupRuleTargetComponent> buildTargetList(StructureMap.StructureMapGroupRuleTargetComponent[] sources) {
List<StructureMap.StructureMapGroupRuleTargetComponent> retVal = new ArrayList<StructureMap.StructureMapGroupRuleTargetComponent>();
retVal.addAll(Arrays.asList(sources));
return retVal;
}
public List<StructureMap.StructureMapGroupComponent> buildTestGroup() throws Exception {
List<StructureMap.StructureMapGroupComponent> retVal = new ArrayList<StructureMap.StructureMapGroupComponent>();
StructureMap.StructureMapGroupComponent group = new StructureMap.StructureMapGroupComponent();
group.setName("TestStructureToCoding");
group.setTypeMode(StructureMap.StructureMapGroupTypeMode.TYPEANDTYPES);
group.setInput(this.buildTestInput());
group.setRule(this.buildTestRules());
retVal.add(group);
return retVal;
}
public List<StructureMap.StructureMapGroupInputComponent> buildTestInput() {
List<StructureMap.StructureMapGroupInputComponent> retVal = new ArrayList<StructureMap.StructureMapGroupInputComponent>();
StructureMap.StructureMapGroupInputComponent sourceIn = new StructureMap.StructureMapGroupInputComponent();
StructureMap.StructureMapGroupInputComponent targetIn = new StructureMap.StructureMapGroupInputComponent();
sourceIn.setName("source");
sourceIn.setType("TestStructure");
sourceIn.setMode(StructureMap.StructureMapInputMode.SOURCE);
targetIn.setName("target");
targetIn.setType("http://hl7.org/fhir/StructureDefinition/Coding");
targetIn.setMode(StructureMap.StructureMapInputMode.TARGET);
retVal.add(sourceIn);
retVal.add(targetIn);
return retVal;
}
public List<StructureMap.StructureMapGroupRuleComponent> buildTestRules() throws Exception {
List<StructureMap.StructureMapGroupRuleComponent> retVal = new ArrayList<StructureMap.StructureMapGroupRuleComponent>();
StructureMap.StructureMapGroupRuleComponent codingSystem = new StructureMap.StructureMapGroupRuleComponent();
StructureMap.StructureMapGroupRuleComponent codingExtension = new StructureMap.StructureMapGroupRuleComponent();
codingSystem.setName("Coding.System");
codingSystem.setSource(this.buildSourceList(new StructureMap.StructureMapGroupRuleSourceComponent[] {
this.buildSource("source", "system", "v", null, null, null)
}));
codingSystem.setTarget(this.buildTargetList(new StructureMap.StructureMapGroupRuleTargetComponent[] {
this.buildTarget("target", "system", null, StructureMap.StructureMapTransform.COPY, new TargetParam[] { new TargetParam("Id", "v") })
}));
codingExtension.setName("Coding.Extension");
codingExtension.setSource(this.buildSourceList(new StructureMap.StructureMapGroupRuleSourceComponent[] {
this.buildSource("source", "system", "v", null, null, null)
}));
codingExtension.setTarget(this.buildTargetList(new StructureMap.StructureMapGroupRuleTargetComponent[] {
this.buildTarget("target", "extension", "ex", null, new TargetParam[] { new TargetParam("", "") }),
this.buildTarget("ex", "url", null, StructureMap.StructureMapTransform.COPY, new TargetParam[] { new TargetParam("Id", "v") }),
this.buildTarget("ex", "value", null, StructureMap.StructureMapTransform.COPY, new TargetParam[] { new TargetParam("String", "v") })
}));
retVal.add(codingSystem);
retVal.add(codingExtension);
return retVal;
}
public List<StructureMap.StructureMapGroupRuleTargetParameterComponent> constructParameters(TargetParam[] params) throws Exception {
List<StructureMap.StructureMapGroupRuleTargetParameterComponent> parameterComponents = new ArrayList<StructureMap.StructureMapGroupRuleTargetParameterComponent>();
for (TargetParam tp : params) {
if (tp.getType() == "Id") // TODO: Convert TypeParam.Type into an Enum.
parameterComponents.add(new StructureMap.StructureMapGroupRuleTargetParameterComponent().setValue(new IdType().setValue(tp.getValue())));
else if (tp.getType() == "String")
parameterComponents.add(new StructureMap.StructureMapGroupRuleTargetParameterComponent().setValue((new StringType().setValue(tp.getValue()))));
else if (tp.getType() == "Boolean") {
boolean bValue = Boolean.getBoolean(tp.getValue());
parameterComponents.add(new StructureMap.StructureMapGroupRuleTargetParameterComponent().setValue(new BooleanType().setValue(bValue)));
} else if (tp.getType() == "Integer") {
int iValue = Integer.getInteger(tp.getValue());
parameterComponents.add(new StructureMap.StructureMapGroupRuleTargetParameterComponent().setValue(new IntegerType().setValue(iValue)));
} else if (tp.getType() == "Decimal") {
long lValue = Long.getLong(tp.getValue());
parameterComponents.add(new StructureMap.StructureMapGroupRuleTargetParameterComponent().setValue(new DecimalType(lValue)));
}
}
return parameterComponents;
}
public List<StructureMap.StructureMapStructureComponent> createMapStructureList() {
List<StructureMap.StructureMapStructureComponent> retVal = new ArrayList<StructureMap.StructureMapStructureComponent>();
StructureMap.StructureMapStructureComponent source = new StructureMap.StructureMapStructureComponent();
StructureMap.StructureMapStructureComponent target = new StructureMap.StructureMapStructureComponent();
source.setUrl("http://opencimi.org/structuredefinition/TestStructure");
source.setMode(StructureMap.StructureMapModelMode.SOURCE);
target.setUrl("http://hl7.org/fhir/StructureDefinition/Coding");
target.setMode(StructureMap.StructureMapModelMode.TARGET);
retVal.add(source);
retVal.add(target);
return retVal;
}
public StructureDefinition.StructureDefinitionDifferentialComponent createTestDiff() {
StructureDefinition.StructureDefinitionDifferentialComponent retVal = new StructureDefinition.StructureDefinitionDifferentialComponent();
List<ElementDefinition> eList = new ArrayList<ElementDefinition>();
ElementDefinition ed0 = new ElementDefinition();
// ElementDefinition.ElementDefinitionBaseComponent base = new ElementDefinition.ElementDefinitionBaseComponent();
// base.setId("http://hl7.org/fhir/StructureDefinition/Element");
ed0.setId("TestStructure");
ed0.setSliceName("TestStructure");
ed0.setPath("TestStructure");
// ed0.setBase(base);
ed0.setMin(1);
ed0.setMax("1");
eList.add(ed0);
ElementDefinition ed = new ElementDefinition();
// ElementDefinition.ElementDefinitionBaseComponent base = new ElementDefinition.ElementDefinitionBaseComponent();
// base.setId("http://hl7.org/fhir/StructureDefinition/Element");
ed.setId("system");
ed.setSliceName("system");
ed.setPath("TestStructure.system");
// ed.setBase(base);
ed.setFixed(new UriType().setValue("HTTP://opencimi.org/structuredefinition/TestStructure.html#Debugging"));
// ed.setType(this.createTypeRefList());
eList.add(ed);
retVal.setElement(eList);
return retVal;
}
public StructureDefinition.StructureDefinitionSnapshotComponent createTestSnapshot() {
StructureDefinition.StructureDefinitionSnapshotComponent retVal = new StructureDefinition.StructureDefinitionSnapshotComponent();
List<ElementDefinition> eList = new ArrayList<ElementDefinition>();
ElementDefinition ed0 = new ElementDefinition();
// ElementDefinition.ElementDefinitionBaseComponent base = new ElementDefinition.ElementDefinitionBaseComponent();
// base.setId("http://hl7.org/fhir/StructureDefinition/Element");
ed0.setId("TestStructure");
ed0.setSliceName("TestStructure");
ed0.setPath("TestStructure");
// ed0.setBase(base);
ed0.setMin(1);
ed0.setMax("1");
eList.add(ed0);
ElementDefinition ed = new ElementDefinition();
// ElementDefinition.ElementDefinitionBaseComponent base = new ElementDefinition.ElementDefinitionBaseComponent();
// base.setId("http://hl7.org/fhir/StructureDefinition/Element");
ed.setId("system");
ed.setSliceName("system");
ed.setPath("TestStructure.system");
// ed.setBase(base);
ed.setFixed(new UriType().setValue("HTTP://opencimi.org/structuredefinition/TestStructure.html#Debugging"));
// ed.setType(this.createTypeRefList());
ed.setMin(1);
ed.setMax("1");
eList.add(ed);
retVal.setElement(eList);
return retVal;
}
public StructureDefinition createTestStructure() {
StructureDefinition sd = new StructureDefinition();
sd.setId("TestStructure");
sd.setUrl("http://opencimi.org/structuredefinition/TestStructure");
sd.setStatus(Enumerations.PublicationStatus.DRAFT);
sd.setName("TestStructure");
sd.setType("TestStructure");
sd.setSnapshot(this.createTestSnapshot());
sd.setDifferential(this.createTestDiff());
sd.setKind(StructureDefinition.StructureDefinitionKind.LOGICAL);
return sd;
}
public StructureMap createTestStructuremap() throws Exception {
StructureMap retMap = new StructureMap();
retMap.setUrl("http://opencimi.org/structuremap/testtransform");
retMap.setName("TestTransform");
retMap.setStatus(Enumerations.PublicationStatus.DRAFT);
retMap.setStructure(this.createMapStructureList());
retMap.setGroup(this.buildTestGroup());
return retMap;
}
/**
* Sets up the resource paths as well as create the contexts using a defalut validator to start with.
*
* @throws Exception
*/
@Before
public void setUp() throws Exception {
if (this.context == null) {
this.context = FhirContext.forDstu3();
}
}
/**
* See #682
*/
@Test
public void testMappingTransform() throws Exception {
Map<String, StructureMap> maps = new HashMap<String, StructureMap>(); // Instantiate a hashmap for StructureMaps
this.validationSupport = new PrePopulatedValidationSupport(); // Create Validation Instance
for (StructureDefinition sd : new DefaultProfileValidationSupport().fetchAllStructureDefinitions(this.context)) { // Read in the default Structure Definitions into a validator that allows custom
// declared structure definitions.
this.validationSupport.addStructureDefinition(sd);
}
StructureDefinition sd1 = this.createTestStructure(); // Calls a method that constructs a comp
this.validationSupport.addStructureDefinition(sd1); // Add custom structure to validation support.
this.hapiContext = new HapiWorkerContext(this.context, this.validationSupport);// Init the Hapi Work
StructureMap map = this.createTestStructuremap();
maps.put(map.getUrl(), map);
StructureMapUtilities scu = new StructureMapUtilities(hapiContext, maps, null, null);
List<StructureDefinition> result = scu.analyse(null, map).getProfiles();
assertEquals(1, result.size());
ourLog.info(context.newXmlParser().setPrettyPrint(true).encodeResourceToString(result.get(0)));
}
public class TargetParam {
private String type;
private String value;
public TargetParam(String type, String value) {
this.type = type;
this.value = value;
}
public String getType() {
return type;
}
public String getValue() {
return value;
}
public void setType(String type) {
this.type = type;
}
public void setValue(String value) {
this.value = value;
}
}
}

View File

@ -47,7 +47,7 @@
</snapshots>
<id>bintray-dnault-maven</id>
<name>bintray</name>
<url>http://dl.bintray.com/dnault/maven</url>
<url>https://dl.bintray.com/dnault/maven</url>
</repository>
<repository>
<id>jitpack.io</id>
@ -337,6 +337,10 @@
<id>ohr</id>
<name>Christian Ohr</name>
</developer>
<developer>
<id>eug48</id>
<name>Eugene Lubarsky</name>
</developer>
</developers>
<licenses>

View File

@ -161,6 +161,31 @@
XML encoding because of the browser Accept header even though
this was not what the client wanted.
</action>
<action type="add" issue="651">
Enhancement to ResponseHighlighterInterceptor where links in the resource
body are now converted to actual clickable hyperlinks. Thanks to Eugene Lubarsky
for the pull request!
</action>
<action type="add">
BanUnsupportedHttpMethodsInterceptor has been modified so that it now allows
HTTP PATCH to proceed.
</action>
<action type="add" issue="651">
Enhancement to ResponseHighlighterInterceptor so that it now can be configured
to display the request headers and response headers, and individual lines
may be highlighted.
</action>
<action type="fix">
AuthorizationInterceptor did not permit PATCH operations to proceed even
if the user had write access for the resource being patched.
</action>
<action type="fix" issue="682">
Fix an issue in HapiWorkerContext where structure definitions are
not able to be retrieved if they are referred to by their
relative or logical ID. This affects profile tooling such as
StructureMapUtilities. Thanks to Travis Lukach for reporting and
providing a test case!
</action>
</release>
<release version="2.5" date="2017-06-08">
<action type="fix">

View File

@ -11,16 +11,16 @@
<a name="operations" />
<p>
This page shows the operations which can be implemented on
This page shows the operations which can be implemented on
HAPI
<a href="./doc_rest_server.html">RESTful Servers</a>, as well as
<a href="./doc_rest_client_annotation.html">Annotation Clients</a>.
Most of the examples shown here show how to implement a server
method, but to perform an equivalent call on an annotation
client you simply put a method with the same signature in your
client interface.
client interface.
</p>
<a name="instance_read" />
</section>
@ -80,7 +80,7 @@
operation retrieves a specific version of a resource with a given ID.
To support vread, simply add "version=true" to your @Read annotation. This
means that the read method will support both "Read" and "VRead". The IdDt
may or may not have the version populated depending on the client request.
may or may not have the version populated depending on the client request.
</p>
<macro name="snippet">
@ -93,7 +93,7 @@
<br />
<code>http://fhir.example.com/Patient/111/_history/2</code>
</p>
<a name="instance_update" />
</section>
@ -156,14 +156,14 @@
<param name="id" value="updateClient" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<h4>Conditional Updates</h4>
<p>
If you wish to suport conditional updates, you can add a parameter
tagged with a
tagged with a
<a href="./apidocs/ca/uhn/fhir/rest/annotation/ConditionalOperationParam.html">@ConditionalOperationParam</a>
annotation. If the request URL contains search parameters instead of a
resource ID, then this parameter will be populated.
resource ID, then this parameter will be populated.
</p>
<macro name="snippet">
@ -185,7 +185,7 @@
for any reason, you may also add parameters which have been annotated
with the <code>@ResourceParam</code> of type
<code>String</code> (to access the raw resource body) and/or
<code>EncodingEnum</code> (to determine which encoding was used)
<code>EncodingEnum</code> (to determine which encoding was used)
</p>
<p>
The following example shows how to use these additonal data elements.
@ -213,35 +213,35 @@
server method should add the updated resource to the MethodOutcome object
being returned, as shown in the example below.
</p>
<macro name="snippet">
<param name="id" value="updatePrefer" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<h4>Contention Aware Updating</h4>
<p>
As of FHIR DSTU2, FHIR uses the <code>ETag</code> header to
provide "conention aware updating". Under this scheme, a client
may create a request that contains an ETag specifying the version,
and the server will fail if the given version is not the latest
and the server will fail if the given version is not the latest
version.
</p>
<p>
Such a request is shown below. In the following example, the update will
only be applied if resource "Patient/123" is currently at version "3".
Otherwise,
Otherwise,
</p>
<pre><![CDATA[PUT [serverBase]/Patient/123
If-Match: W/"3"]]></pre>
<p>
If a client performs a contention aware update, the ETag version will be
placed in the version part of the IdDt/IdType that is passed into the
placed in the version part of the IdDt/IdType that is passed into the
method. For example:
</p>
<macro name="snippet">
<param name="id" value="updateEtag" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
@ -305,14 +305,14 @@ If-Match: W/"3"]]></pre>
</p>
<h4>Conditional Deletes</h4>
<p>
The FHIR specification also allows "conditional deletes". A conditional
delete uses a search style URL instead of a read style URL, and
deletes a single resource if it matches the given search parameters.
The following example shows how to
The following example shows how to
</p>
<macro name="snippet">
<param name="id" value="deleteConditional" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
@ -382,7 +382,7 @@ If-Match: W/"3"]]></pre>
</macro>
<h4>Conditional Creates</h4>
<p>
The FHIR specification also allows "conditional creates". A conditional
create has an additional header called <code>If-None-Exist</code>
@ -398,7 +398,7 @@ If-Match: W/"3"]]></pre>
<a href="./apidocs/ca/uhn/fhir/rest/annotation/ConditionalOperationParam.html">@ConditionalOperationParam</a>
is detected, it will be populated with the value of this header.
</p>
<macro name="snippet">
<param name="id" value="createConditional" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
@ -409,21 +409,21 @@ If-Match: W/"3"]]></pre>
<br />
<code>http://fhir.example.com/Patient<br/>If-None-Exist: Patient?identifier=system%7C0001</code>
</p>
<h4>Prefer Header / Returning the resource body</h4>
<p>
If you wish to allow your server to honour the <code>Prefer</code>
header, the same mechanism shown above for
header, the same mechanism shown above for
<a href="#prefer">Prefer Header for Updates</a> should be used.
</p>
<h4>Accessing The Raw Resource Payload</h4>
<p>
The create operation also supports access to the raw payload,
using the same semantics as raw payload access
using the same semantics as raw payload access
<a href="#raw_update_access">for the update operation</a>.
</p>
<a name="type_search" />
</section>
@ -482,9 +482,9 @@ If-Match: W/"3"]]></pre>
individual HAPI resource
classes.
</p>
<p>
Parameters which take a string as their format should use the
Parameters which take a string as their format should use the
<code><a href="./apidocs/ca/uhn/fhir/rest/param/StringParam.html">StringParam</a></code>
type. They may also use normal java <code>String</code>, although it is
not possible to use the <code>:exact</code> qualifier in that case.
@ -671,21 +671,21 @@ If-Match: W/"3"]]></pre>
for patients.
</p>
<p>
Reference parameters use the
<a href="./apidocs/ca/uhn/fhir/rest/param/ReferenceParam.html">ReferenceParam</a>
Reference parameters use the
<a href="./apidocs/ca/uhn/fhir/rest/param/ReferenceParam.html">ReferenceParam</a>
type. Reference parameters are, in their most basic form, just a pointer to another
resource. For example, you might want to query for DiagnosticReport resources where the
subject (the Patient resource that the report is about) is Patient/123. The following
example shows a simple resource reference parameter in use.
example shows a simple resource reference parameter in use.
</p>
<macro name="snippet">
<param name="id" value="referenceSimple" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<h4>Chained Resource References</h4>
<p>
References may also support a "chained" value. This is a search parameter name
on the target resource. For example, you might want to search for DiagnosticReport
@ -697,20 +697,20 @@ If-Match: W/"3"]]></pre>
where the <b>subject</b> (Patient) of the report has the <b>family</b> (name) of
'SMITH'</i>".
</p>
<p>
There are two ways of dealing with chained parameters in your methods: static chains and
dynamic chains. Both are equally valid, although dyamic chains might lead to somewhat
more compact and readable code.
</p>
<a name="dynamic_chains"/>
<h4>Dynamic Chains</h4>
<p>
Chained values must be explicitly declared through the use
of a whitelist (or blacklist). The following example shows how to declare a
report with an allowable chained parameter:
report with an allowable chained parameter:
</p>
<macro name="snippet">
<param name="id" value="referenceWithChain" />
@ -720,7 +720,7 @@ If-Match: W/"3"]]></pre>
<p>
You may also specify the whitelist value of
<code>""</code> to allow an empty chain (e.g. ther resource ID)
and this can be combined with other values, as shown below:
and this can be combined with other values, as shown below:
</p>
<macro name="snippet">
@ -738,7 +738,7 @@ If-Match: W/"3"]]></pre>
<param name="id" value="referenceWithDynamicChain" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<h4>Static Chains</h4>
<p>
@ -766,11 +766,11 @@ If-Match: W/"3"]]></pre>
<p>
In the following example, Observation.name-value-date is shown. This parameter
is a composite of a string and a date. Note that the composite parameter types
(StringParam and DateParam) must be specified in both the annotation's
(StringParam and DateParam) must be specified in both the annotation's
<code>compositeTypes</code> field, as well as the generic types for the
<code>CompositeParam</code> method parameter itself.
</p>
<macro name="snippet">
<param name="id" value="searchComposite" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
@ -810,23 +810,23 @@ If-Match: W/"3"]]></pre>
<p>
If you wish to create a server that can accept any combination of a large number
of parameters, (this is how the various reference servers behave, as well as the
<a href="http://fhirtest.uhn.ca">public HAPI server</a>)
of parameters, (this is how the various reference servers behave, as well as the
<a href="http://fhirtest.uhn.ca">public HAPI server</a>)
the easiest way to accomplish this is to simply create one method
with all allowable parameters, each annotated as @OptionalParam.
</p>
<p>
On the other hand, if you have specific combinations of parameters you wish to
On the other hand, if you have specific combinations of parameters you wish to
support (a common scenario if you are building FHIR on top of existing data sources
and only have certain indexes you can use) you could create multiple search methods,
each with specific required and optional parameters matching the database indexes.
</p>
<p>
The following example shows a method with two parameters.
</p>
<macro name="snippet">
<param name="id" value="searchOptionalParam" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
@ -881,7 +881,7 @@ If-Match: W/"3"]]></pre>
<p>
It is worth noting that according to the FHIR specification, you can have an
AND relationship combining multiple OR relationships, but not vice-versa. In
other words, it's possible to support a search like
other words, it's possible to support a search like
<code>("name" = ("joe" or "john")) AND ("age" = (11 or 12))</code> but not
a search like
<code>("language" = ("en" AND "fr") OR ("address" = ("Canada" AND "Quebec"))</code>
@ -892,7 +892,7 @@ If-Match: W/"3"]]></pre>
<p>
To accept a composite parameter, use a parameter type which implements the
<a href="./apidocs/ca/uhn/fhir/model/api/IQueryParameterOr.html">IQueryParameterOr</a>
interface.
interface.
</p>
<p>
Each parameter type (StringParam, TokenParam, etc.) has a corresponding parameter
@ -924,7 +924,7 @@ If-Match: W/"3"]]></pre>
interface (which in turn encapsulates the corresponding IQueryParameterOr types).
</p>
<p>
An example follows which shows a search for Patients by address, where multiple string
An example follows which shows a search for Patients by address, where multiple string
lists may be supplied by the client. For example, the client might request that the
address match <code>("Montreal" OR "Sherbrooke") AND ("Quebec" OR "QC")</code> using
the following query:
@ -932,11 +932,11 @@ If-Match: W/"3"]]></pre>
<code>http://fhir.example.com/Patient?address=Montreal,Sherbrooke&amp;address=Quebec,QC</code>
</p>
<p>
The following code shows how to receive this parameter using a
The following code shows how to receive this parameter using a
<a href="./apidocs/ca/uhn/fhir/rest/param/StringAndListParam.html">StringAndListParameter</a>,
which can handle an AND list of multiple OR lists of strings.
</p>
<macro name="snippet">
<param name="id" value="searchMultipleAnd" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
@ -948,13 +948,13 @@ If-Match: W/"3"]]></pre>
to use AND search parameters to specify a search criteria of
<code>(A=1 OR A=2) AND (B=1 OR B=2)</code>
but it is not possible to specify
<code>(A=1 AND B=1) OR (A=2 AND B=2)</code> (aside from
in very specific cases where a composite parameter has been
specifically defined).
<code>(A=1 AND B=1) OR (A=2 AND B=2)</code> (aside from
in very specific cases where a composite parameter has been
specifically defined).
</p>
<h4>AND Relationship Query Parameters for Dates</h4>
<p>
Dates are a bit of a special case, since it is a common scenario to want to match
a date range (which is really just an AND query on two qualified date parameters).
@ -1006,7 +1006,7 @@ If-Match: W/"3"]]></pre>
<p>
To add support for reverse includes (via the <code>_revinclude</code> parameter),
use the same format as with the <code>_include</code> parameter (shown above)
use the same format as with the <code>_include</code> parameter (shown above)
but add <code>reverse=true</code> to the <code>@IncludeParam</code>
annotation, as shown below.
</p>
@ -1017,7 +1017,7 @@ If-Match: W/"3"]]></pre>
</macro>
</subsection>
<subsection name="Named Queries (_query)">
<p>
@ -1055,15 +1055,15 @@ If-Match: W/"3"]]></pre>
</p>
<p>
According to the specification, sorting is requested by the client using a
According to the specification, sorting is requested by the client using a
search param as the sort key. For example, when searching Patient resources,
a sort key of "given" requests the "given" search param as the sort key. That
param maps to the values in the field "Patient.name.given".
param maps to the values in the field "Patient.name.given".
</p>
<p>
Sort specifications can be passed into handler methods by adding a parameter
of type
of type
SortSpec,
which has been annotated with the
@Sort
@ -1087,9 +1087,9 @@ If-Match: W/"3"]]></pre>
<p>
It is also possible to annotate search methods and/or parameters with
the
the
<a href="./apidocs/ca/uhn/fhir/model/api/annotation/Description.html">@Description</a>
annotation. This annotation allows you to add a description of the method
annotation. This annotation allows you to add a description of the method
and the individual parameters. These descriptions will be placed in the
server's conformance statement, which can be helpful to anyone who is developing
software against your server.
@ -1101,7 +1101,7 @@ If-Match: W/"3"]]></pre>
</macro>
</subsection>
<a name="type_validate" />
</section>
@ -1124,7 +1124,7 @@ If-Match: W/"3"]]></pre>
In FHIR DSTU1 the validate operation used a URL resembling <code>http://example.com/Patient/_validate</code>
with a resource in the HTTP POST body. In FHIR DSTU2, validate has been changed to use the
<a href="#extended_operations">extended operation</a> mechanism. It now uses a URL
resembling <code>http://example.com/Patient/$validate</code> and takes a
resembling <code>http://example.com/Patient/$validate</code> and takes a
Parameters resource as input in the method body.<br/><br/>
The mechanism described below may be used for both DSTU1 and DSTU2+ servers, and HAPI
will automatically use the correct form depending on what FHIR version the
@ -1180,13 +1180,13 @@ If-Match: W/"3"]]></pre>
<p>
In the example above, only the <code>@ResourceParam</code> parameter is technically required, but
in DSTU2 you are encouraged to also add the following parameters:
in DSTU2 you are encouraged to also add the following parameters:
</p>
<ul>
<li><b>@Validate.Mode ValidationModeEnum mode</b> - This is the validation mode (see the FHIR specification for information on this)</li>
<li><b>@Validate.Profile String profile</b> - This is the profile to validate against (see the FHIR specification for more information on this)</li>
</ul>
<p>
Example URL to invoke this method (this would be invoked using an HTTP POST,
with a Parameters resource in the POST body):
@ -1260,7 +1260,7 @@ If-Match: W/"3"]]></pre>
<section name="System Level - Transaction">
<p>
The
The
<a href="http://hl7.org/implement/standards/fhir/http.html#transaction">transaction</a>
action is among the most challenging parts of the FHIR specification to implement. It allows the
user to submit a bundle containing a number of resources to be created/updated/deleted as a single
@ -1276,35 +1276,35 @@ If-Match: W/"3"]]></pre>
<param name="id" value="transaction" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<p>
Transaction methods require one parameter annotated with @TransactionParam, and that
parameter may be of type List&lt;IResource&gt; or Bundle.
</p>
<p>
In terms of actually implementing the method, unfortunately there is only so much help
HAPI will give you. One might expect HAPI to automatically delegate the individual
operations in the transaction to other methods on the server but at this point it
does not do that. There is a lot that transaction needs to handle
(making everything atomic, replacing placeholder IDs across multiple resources
which may even be circular, handling operations in the right order) and
In terms of actually implementing the method, unfortunately there is only so much help
HAPI will give you. One might expect HAPI to automatically delegate the individual
operations in the transaction to other methods on the server but at this point it
does not do that. There is a lot that transaction needs to handle
(making everything atomic, replacing placeholder IDs across multiple resources
which may even be circular, handling operations in the right order) and
so far we have not found a way for the framework to do this in a generic way.
</p>
<p>
What it comes down to is the fact that transaction is a tricky thing to implement.
For what it's worth, you could look at our JPA module's "transaction" method in
<a href="https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java">our source repository</a>
<a href="https://github.com/jamesagnew/hapi-fhir/blob/master/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/dstu3/FhirSystemDaoDstu3.java">our source repository</a>
to see how we implemented transaction in the JPA server.
</p>
<p>
Example URL to invoke this method:
<br />
<code>POST http://fhir.example.com/</code><br/>
<i>(note that the content of this POST will be a bundle)</i>
</p>
<a name="system_search" />
</section>
@ -1414,13 +1414,13 @@ If-Match: W/"3"]]></pre>
<u>
<li>
The <code>@Since</code> method argument implements the <code>_since</code>
parameter and should be of type <code>DateTimeDt</code> or <code>DateTimeType</code>
parameter and should be of type <code>DateTimeDt</code> or <code>DateTimeType</code>
</li>
<li>
The <code>@At</code> method argument implements the <code>_at</code>
parameter and may be of type
parameter and may be of type
<code>DateRangeParam</code>,
<code>DateTimeDt</code> or <code>DateTimeType</code>
<code>DateTimeDt</code> or <code>DateTimeType</code>
</li>
</u>
@ -1441,6 +1441,29 @@ If-Match: W/"3"]]></pre>
<a name="exceptions" />
</section>
<section name="Instance Level - Patch">
<p>
HAPI FHIR includes basic support for the
<a href="http://hl7.org/implement/standards/fhir/http.html#patch">
<b>patch</b>
</a>
operation. This support allows you to perform patches, but does not
include logic to actually implement resource patching in the server
framework (note that the JPA server does include a patch implementation).
</p>
<p>
The following snippet shows how to define a patch method on a server:
</p>
<macro name="snippet">
<param name="id" value="patch" />
<param name="file" value="examples/src/main/java/example/PatchExamples.java" />
</macro>
</patch>
<!-- ****************************************************************** -->
<!-- ****************************************************************** -->
<!-- ****************************************************************** -->
@ -1496,16 +1519,16 @@ If-Match: W/"3"]]></pre>
<a href="http://hl7.org/implement/standards/fhir/extras.html#tags">here</a>
before attempting to implement tagging in your own applications.
</p>
<subsection name="Accessing Tags in a Read / VRead / Search Method">
<p>
Tags are stored within a resource object, in the
Tags are stored within a resource object, in the
<a href="./apidocs/ca/uhn/fhir/model/api/IResource.html#getResourceMetadata()">IResource.html#getResourceMetadata()</a>
map, under the key
map, under the key
<a href="./apidocs/ca/uhn/fhir/model/api/ResourceMetadataKeyEnum.html#TAG_LIST">TAG_LIST</a>.
</p>
<p>
In a server implementation, you may populate your tags into the
returned resource(s) and HAPI will automatically place these tags into
@ -1514,39 +1537,39 @@ If-Match: W/"3"]]></pre>
example shows how to supply tags in a read method, but the same approach applies
to vread and search operations as well.
</p>
<macro name="snippet">
<param name="id" value="readTags" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<p>
In a client operation, you simply call the read/vread/search method as you
normally would (as described above), and if any tags have been returned
by the server, these may be accessed from the resource metadata.
</p>
<macro name="snippet">
<param name="id" value="clientReadTags" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
</subsection>
<subsection name="Setting Tags in a Create/Update Method">
<p>
Within a <a href="#type_create">Type Create</a>
or <a href="#instance_update">Instance Update</a> method, it is
possible for the client to specify a set of tags to be stored
along with the saved resource instance.
along with the saved resource instance.
</p>
<p>
Note that FHIR specifies that in an update method, any tags supplied
by the client are copied to the newly saved version, as well as any
tags the existing version had.
</p>
<p>
To work with tags in a create/update method, the pattern used in the
read examples above is simply revered. In a server, the resource which
@ -1557,16 +1580,16 @@ If-Match: W/"3"]]></pre>
<param name="id" value="createTags" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
</subsection>
<subsection name="More tag methods">
<p>
FHIR also provides a number of operations to interact directly
with tags. These methods may be used to retrieve lists of tags
that are available on the server, or to add or remove tags from
resources without interacting directly with those resources.
resources without interacting directly with those resources.
</p>
<p>
@ -1577,14 +1600,14 @@ If-Match: W/"3"]]></pre>
<param name="id" value="tagMethodProvider" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<p>
On a client, the methods are defined in the exact same way, except that
there is no method body in the client interface.
</p>
</subsection>
</section>
<!-- ****************************************************************** -->
@ -1592,18 +1615,18 @@ If-Match: W/"3"]]></pre>
<!-- ****************************************************************** -->
<section name="_summary and _elements">
The <code>_summary</code> and <code>_elements</code> parameters are
automatically handled by the server, so no coding is required to make this
work. If you wish to add parameters to manually handle these fields however,
the following example shows how to access these.
<macro name="snippet">
<param name="id" value="summaryAndElements" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<a name="compartments" />
</section>
@ -1619,7 +1642,7 @@ If-Match: W/"3"]]></pre>
</p>
<p>
To define a search by compartment, you simply need to add the <code>compartmentName</code> attribute
to the <code>@Search</code> annotation, and add an <code>@IdParam</code> parameter.
to the <code>@Search</code> annotation, and add an <code>@IdParam</code> parameter.
</p>
<p>
The following example shows a search method in a resource provider which returns
@ -1630,40 +1653,40 @@ If-Match: W/"3"]]></pre>
<param name="id" value="searchCompartment" />
<param name="file" value="examples/src/main/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<p>
Example URL to invoke this method:
<br />
<code>http://fhir.example.com/Patient/123/Condition</code>
</p>
<a name="extended_operations"/>
</section>
<section name="Extended Operations">
<p>
FHIR extended operations are a special type of RPC-style invocation you
can perform against a FHIR server, type, or resource instance. These invocations
take a
take a
<a href="./apidocs-dstu2/ca/uhn/fhir/model/dstu2/resource/Parameters.html">Parameters</a>
resource as input, and return either another Parameters resource or a different resource type.
</p>
<p>
To define an operation, a method should be placed in a
<a href="./doc_rest_server.html#resource_providers">Resource Provider</a>
To define an operation, a method should be placed in a
<a href="./doc_rest_server.html#resource_providers">Resource Provider</a>
class if the operation works against a resource type (e.g. <code>Patient</code>)
or a resource instance (e.g. <code>Patient/123</code>), or on a
Plain Provider
or a resource instance (e.g. <code>Patient/123</code>), or on a
Plain Provider
if the operation works against the server (i.e. it is global and not resource specific).
</p>
<subsection name="Type-Specific Operations">
<p>
To implement a type-specific operation,
the method should be annotated with the
To implement a type-specific operation,
the method should be annotated with the
<code>@Operation</code> tag, and should have an
<code>@OperationParam</code> tag for each named parameter that
the input Parameters resource may be populated with. The following
@ -1675,17 +1698,17 @@ If-Match: W/"3"]]></pre>
<param name="file"
value="examples/src/main/java/example/ServerOperations.java" />
</macro>
<p>
Example URL to invoke this operation (HTTP request body is Parameters resource):
<br />
<code>POST http://fhir.example.com/Patient/$everything</code>
</p>
</subsection>
<subsection name="Instance-Specific Operations">
<p>
To create an instance-specific operation (an operation which takes the
ID of a specific resource instance as a part of its request URL),
@ -1698,25 +1721,25 @@ If-Match: W/"3"]]></pre>
<param name="file"
value="examples/src/main/java/example/ServerOperations.java" />
</macro>
<p>
Example URL to invoke this operation (HTTP request body is Parameters resource):
<br />
<code>http://fhir.example.com/Patient/123/$everything</code>
</p>
</subsection>
<subsection name="Using Search Parameter Types">
<p>
FHIR allows operation parameters to be of a
FHIR allows operation parameters to be of a
<a href="http://hl7.org/fhir/search.html#ptypes">Search parameter type</a>
(e.g. token) instead of a FHIR datatype (e.g. Coding).
</p>
<p>
To use a search parameter type, any of the search parameter
types listed in
To use a search parameter type, any of the search parameter
types listed in
<a href="./doc_rest_operations.html#Type_Level_-_Search">Search</a>
may be used. For example, the following is a simple operation method declaration
using search parameters:
@ -1726,7 +1749,7 @@ If-Match: W/"3"]]></pre>
<param name="file"
value="examples/src/main/java/example/ServerOperations.java" />
</macro>
<p>
Example URL to invoke this operation (HTTP request body is Parameters resource):
<br />
@ -1738,10 +1761,10 @@ If-Match: W/"3"]]></pre>
if you want to be able to accept multiple values. For example,
a <code>List&lt;TokenParam&gt;</code> could be used if you want
to allow multiple repetitions of a given token parameter (this is
analogous to the "AND" semantics in a search).
analogous to the "AND" semantics in a search).
A <code>TokenOrListParam</code> could be used if you want to allow
multiple values within a single repetition, separated by comma (this
is analogous to "OR" semantics in a search).
is analogous to "OR" semantics in a search).
</p>
<p>For example:</p>
<macro name="snippet">
@ -1749,14 +1772,14 @@ If-Match: W/"3"]]></pre>
<param name="file"
value="examples/src/main/java/example/ServerOperations.java" />
</macro>
</subsection>
<subsection name="Server Operations">
<p>
Server operations do not operate on a specific resource type or
instance, but rather operate globally on the server itself. The following
instance, but rather operate globally on the server itself. The following
example show how to implement the
<code>$closure</code> operation. Note that the <code>concept</code> parameter
in the example has a cardinality of <code>0..*</code> according to the
@ -1768,30 +1791,30 @@ If-Match: W/"3"]]></pre>
<param name="file"
value="examples/src/main/java/example/ServerOperations.java" />
</macro>
<p>
Example URL to invoke this operation (HTTP request body is Parameters resource):
<br />
<code>http://fhir.example.com/$closure</code>
</p>
</subsection>
<subsection name="Returning Multiple OUT Parameters">
<p>
In all of the Extended Operation examples above, the return
type specified for the operation is a single Resource instance. This is
a common pattern in FHIR defined operations. However, it is also
a common pattern in FHIR defined operations. However, it is also
possible for an extended operation to be defined with multiple
and/or repeating OUT parameters. In this case, you can return
a <code>Parameters</code> resource directly.
a <code>Parameters</code> resource directly.
</p>
</subsection>
<subsection name="Idempotent Operations / Handling HTTP Get">
<p>
The FHIR specification notes that if an operation is
<a href="http://en.wikipedia.org/wiki/Idempotence">idempotent</a>
@ -1801,7 +1824,7 @@ If-Match: W/"3"]]></pre>
</p>
<p>
If you are implementing an operation which is idempotent,
you should mark your operation with
you should mark your operation with
<code>idempotent=true</code>,
as shown in some of the examples above. The default value
for this flag is <code>false</code>, meaning that operations
@ -1814,7 +1837,7 @@ If-Match: W/"3"]]></pre>
server will respond with an <code>HTTP 405 Method Not Supported</code>.
</p>
</subsection>
</section>
</body>