Narrative generator now working

This commit is contained in:
jamesagnew 2014-03-26 17:37:08 -04:00
parent 4953c5629c
commit 082ebb64c1
28 changed files with 457 additions and 112 deletions

View File

@ -35,6 +35,9 @@
---------
Issues:
* Submit _narrative parameter as enhancement suggestion
* Need to be able to bind Identifier.system to a set of allowable values in a profile. Graeme
has suggested making value[x] repeatable in the profile definition

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.model.api;
import ca.uhn.fhir.model.dstu.composite.ContainedDt;
import ca.uhn.fhir.model.dstu.composite.NarrativeDt;
public interface IResource extends ICompositeElement {
@ -15,6 +16,8 @@ public interface IResource extends ICompositeElement {
* </p>
* TODO: document contained resources and link there
*/
public ContainedDt getContained();
ContainedDt getContained();
NarrativeDt getText();
}

View File

@ -7,6 +7,7 @@ import ca.uhn.fhir.model.api.IElement;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.api.annotation.ResourceDef;
import ca.uhn.fhir.model.dstu.composite.ContainedDt;
import ca.uhn.fhir.model.dstu.composite.NarrativeDt;
@ResourceDef(name="Binary", profile="http://hl7.org/fhir/profiles/Binary", id="binary")
public class Binary extends BaseElement implements IResource {
@ -36,4 +37,9 @@ public class Binary extends BaseElement implements IResource {
return null;
}
@Override
public NarrativeDt getText() {
throw new IllegalStateException();
}
}

View File

@ -227,8 +227,11 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
}
@Override
public InputStream getResourceAsStream(TemplateProcessingParameters theTemplateProcessingParameters, String theResourceName) {
String template = myDatatypeClassNameToNarrativeTemplate.get(theResourceName);
public InputStream getResourceAsStream(TemplateProcessingParameters theTemplateProcessingParameters, String theClassName) {
String template = myDatatypeClassNameToNarrativeTemplate.get(theClassName);
if (template==null) {
throw new NullPointerException("No narrative template exists for datatype: " +theClassName);
}
return new ReaderInputStream(new StringReader(template));
}
}
@ -255,10 +258,9 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
final Object value = expression.execute(configuration, theArguments);
Context context = new Context();
QuantityDt datatype = new QuantityDt();
context.setVariable("resource", value);
String result = myDatatypeTemplateEngine.process(datatype.getClass().getCanonicalName(), context);
String result = myDatatypeTemplateEngine.process(value.getClass().getCanonicalName(), context);
Document dom = DOMUtils.getXhtmlDOMFor(new StringReader(result));
theElement.removeAttribute(theAttributeName);

View File

@ -19,6 +19,8 @@ import ca.uhn.fhir.model.primitive.IdDt;
public abstract class BaseParser implements IParser {
private boolean mySuppressNarratives;
@SuppressWarnings("cast")
@Override
public <T extends IResource> T parseResource(Class<T> theResourceType, String theMessageString) {
@ -81,4 +83,19 @@ public abstract class BaseParser implements IParser {
throw new DataFormatException(nextChild + " has no child of type " + type);
}
@Override
public IParser setSuppressNarratives(boolean theSuppressNarratives) {
mySuppressNarratives=theSuppressNarratives;
return this;
}
/**
* If set to <code>true</code> (default is <code>false</code>), narratives will not be included in the
* encoded values.
*/
public boolean getSuppressNarratives() {
return mySuppressNarratives;
}
}

View File

@ -32,4 +32,10 @@ public interface IParser {
IParser setPrettyPrint(boolean thePrettyPrint);
/**
* If set to <code>true</code> (default is <code>false</code>), narratives will not be included in the
* encoded values.
*/
IParser setSuppressNarratives(boolean theSuppressNarratives);
}

View File

@ -143,7 +143,7 @@ public class JsonParser extends BaseParser implements IParser {
@Override
public void encodeResourceToWriter(IResource theResource, Writer theWriter) throws IOException {
Validate.notNull(theResource, "Resource can not be null");
JsonGenerator eventWriter = createJsonGenerator(theWriter);
RuntimeResourceDefinition resDef = myContext.getResourceDefinition(theResource);
@ -245,16 +245,16 @@ public class JsonParser extends BaseParser implements IParser {
}
private static final Set<String> BUNDLE_TEXTNODE_CHILDREN;
static {
HashSet<String> hashSet = new HashSet<String>();
hashSet.add("title");
hashSet.add("id");
hashSet.add("updated");
hashSet.add("published");
BUNDLE_TEXTNODE_CHILDREN=Collections.unmodifiableSet(hashSet);
BUNDLE_TEXTNODE_CHILDREN = Collections.unmodifiableSet(hashSet);
}
private void parseBundleChildren(JsonObject theObject, ParserState<?> theState) {
for (String nextName : theObject.keySet()) {
if ("resourceType".equals(nextName)) {
@ -439,7 +439,8 @@ public class JsonParser extends BaseParser implements IParser {
return eventWriter;
}
private void encodeChildElementToStreamWriter(RuntimeResourceDefinition theResDef, IResource theResource, JsonGenerator theWriter, IElement theValue, BaseRuntimeElementDefinition<?> theChildDef, String theChildName) throws IOException {
private void encodeChildElementToStreamWriter(RuntimeResourceDefinition theResDef, IResource theResource, JsonGenerator theWriter, IElement theValue, BaseRuntimeElementDefinition<?> theChildDef,
String theChildName) throws IOException {
switch (theChildDef.getChildType()) {
case PRIMITIVE_DATATYPE: {
@ -518,11 +519,19 @@ public class JsonParser extends BaseParser implements IParser {
break;
}
case PRIMITIVE_XHTML: {
XhtmlDt dt = (XhtmlDt) theValue;
if (theChildName != null) {
theWriter.write(theChildName, dt.getValueAsString());
if (!getSuppressNarratives()) {
XhtmlDt dt = (XhtmlDt) theValue;
if (theChildName != null) {
theWriter.write(theChildName, dt.getValueAsString());
} else {
theWriter.write(dt.getValueAsString());
}
} else {
theWriter.write(dt.getValueAsString());
if (theChildName != null) {
// do nothing
} else {
theWriter.writeNull();
}
}
break;
}
@ -533,13 +542,14 @@ public class JsonParser extends BaseParser implements IParser {
}
private void encodeCompositeElementChildrenToStreamWriter(RuntimeResourceDefinition theResDef, IResource theResource, IElement theElement, JsonGenerator theEventWriter, List<? extends BaseRuntimeChildDefinition> theChildren) throws IOException {
private void encodeCompositeElementChildrenToStreamWriter(RuntimeResourceDefinition theResDef, IResource theResource, IElement theElement, JsonGenerator theEventWriter,
List<? extends BaseRuntimeChildDefinition> theChildren) throws IOException {
for (BaseRuntimeChildDefinition nextChild : theChildren) {
if (nextChild instanceof RuntimeChildNarrativeDefinition) {
INarrativeGenerator gen = myContext.getNarrativeGenerator();
if (gen != null) {
NarrativeDt narr = gen.generateNarrative(theResDef.getResourceProfile(), theResource);
if (narr!=null) {
if (narr != null) {
RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild;
String childName = nextChild.getChildNameByDatatype(child.getDatatype());
BaseRuntimeElementDefinition<?> type = child.getChildByName(childName);
@ -548,7 +558,7 @@ public class JsonParser extends BaseParser implements IParser {
}
}
}
List<? extends IElement> values = nextChild.getAccessor().getValues(theElement);
if (values == null || values.isEmpty()) {
continue;
@ -672,7 +682,8 @@ public class JsonParser extends BaseParser implements IParser {
}
}
private void encodeCompositeElementToStreamWriter(RuntimeResourceDefinition theResDef, IResource theResource, IElement theElement, JsonGenerator theEventWriter, BaseRuntimeElementCompositeDefinition<?> resDef) throws IOException, DataFormatException {
private void encodeCompositeElementToStreamWriter(RuntimeResourceDefinition theResDef, IResource theResource, IElement theElement, JsonGenerator theEventWriter,
BaseRuntimeElementCompositeDefinition<?> resDef) throws IOException, DataFormatException {
encodeExtensionsIfPresent(theResDef, theResource, theEventWriter, theElement);
encodeCompositeElementChildrenToStreamWriter(theResDef, theResource, theElement, theEventWriter, resDef.getExtensions());
encodeCompositeElementChildrenToStreamWriter(theResDef, theResource, theElement, theEventWriter, resDef.getChildren());
@ -707,7 +718,8 @@ public class JsonParser extends BaseParser implements IParser {
theEventWriter.writeEnd();
}
private void encodeUndeclaredExtensions(RuntimeResourceDefinition theResDef, IResource theResource, JsonGenerator theWriter, List<UndeclaredExtension> extensions, String theTagName) throws IOException {
private void encodeUndeclaredExtensions(RuntimeResourceDefinition theResDef, IResource theResource, JsonGenerator theWriter, List<UndeclaredExtension> extensions, String theTagName)
throws IOException {
if (extensions.isEmpty()) {
return;
}

View File

@ -473,7 +473,7 @@ public class XmlParser extends BaseParser implements IParser {
}
private void encodeXhtml(XhtmlDt theDt, XMLStreamWriter theEventWriter) throws XMLStreamException {
if (theDt == null || theDt.getValue() == null) {
if (theDt == null || theDt.getValue() == null || getSuppressNarratives()) {
return;
}

View File

@ -18,6 +18,14 @@ import ca.uhn.fhir.model.api.IResource;
@Target(value=ElementType.METHOD)
public @interface Search {
/**
* If specified, this the name for the Named Query
*
* @see See the FHIR specification section on
* <a href="http://www.hl7.org/implement/standards/fhir/search.html#advanced">named queries</a>
*/
String queryName() default "";
/**
* The return type for this search method. This generally does not need
* to be populated for a server implementation, since servers will return

View File

@ -88,7 +88,8 @@ public abstract class BaseMethodBinding {
if (read != null) {
return new ReadMethodBinding(methodReturnTypeEnum, returnType, theMethod);
} else if (search != null) {
return new SearchMethodBinding(methodReturnTypeEnum, returnType, theMethod);
String queryName = search.queryName();
return new SearchMethodBinding(methodReturnTypeEnum, returnType, theMethod, queryName);
} else if (conformance != null) {
return new ConformanceMethodBinding(methodReturnTypeEnum, returnType, theMethod);
} else {

View File

@ -1,5 +1,7 @@
package ca.uhn.fhir.rest.method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import ca.uhn.fhir.model.primitive.IdDt;
@ -9,7 +11,7 @@ public class Request {
private IdDt myId;
private String myOperation;
private Set<String> myParameterNames;
private Map<String, String[]> myParameterNames;
private RequestType myRequestType;
private String myResourceName;
private IdDt myVersion;
@ -22,7 +24,7 @@ public class Request {
return myOperation;
}
public Set<String> getParameterNames() {
public Map<String, String[]> getParameterNames() {
return myParameterNames;
}
@ -46,8 +48,8 @@ public class Request {
myOperation = theOperation;
}
public void setParameterNames(Set<String> theParameterNames) {
myParameterNames = theParameterNames;
public void setParameterNames(Map<String, String[]> theParams) {
myParameterNames = theParams;
}
public void setRequestType(RequestType theRequestType) {
@ -62,11 +64,15 @@ public class Request {
myVersion = theVersion;
}
public static Request withResourceAndParams(String theResourceName, RequestType theRequestType, Set<String> theParamNamess) {
public static Request withResourceAndParams(String theResourceName, RequestType theRequestType, Set<String> theParamNames) {
Request retVal = new Request();
retVal.setResourceName(theResourceName);
retVal.setParameterNames(theParamNamess);
retVal.setRequestType(theRequestType);
Map<String, String[]> paramNames = new HashMap<String, String[]>();
for (String next : theParamNames) {
paramNames.put(next, new String[0]);
}
retVal.setParameterNames(paramNames);
return retVal;
}

View File

@ -10,12 +10,15 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationSystemEnum;
import ca.uhn.fhir.model.dstu.valueset.RestfulOperationTypeEnum;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.client.GetClientInvocation;
import ca.uhn.fhir.rest.param.IParameter;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
@ -27,16 +30,16 @@ import ca.uhn.fhir.util.QueryUtil;
public class SearchMethodBinding extends BaseMethodBinding {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class);
private Method method;
private Method myMethod;
private Class<?> myDeclaredResourceType;
private List<IParameter> myParameters;
private String myQueryName;
public SearchMethodBinding(MethodReturnTypeEnum theMethodReturnTypeEnum, Class<? extends IResource> theReturnResourceType, Method theMethod) {
public SearchMethodBinding(MethodReturnTypeEnum theMethodReturnTypeEnum, Class<? extends IResource> theReturnResourceType, Method theMethod, String theQueryName) {
super(theMethodReturnTypeEnum, theReturnResourceType);
this.method = theMethod;
this.myMethod = theMethod;
this.myParameters = Util.getResourceParameters(theMethod);
this.myQueryName = StringUtils.defaultIfBlank(theQueryName, null);
this.myDeclaredResourceType = theMethod.getReturnType();
}
@ -45,7 +48,7 @@ public class SearchMethodBinding extends BaseMethodBinding {
}
public Method getMethod() {
return method;
return myMethod;
}
public List<IParameter> getParameters() {
@ -59,43 +62,50 @@ public class SearchMethodBinding extends BaseMethodBinding {
@Override
public GetClientInvocation invokeClient(Object[] theArgs) throws InternalErrorException {
assert theArgs.length == myParameters.size() : "Wrong number of arguments: " + theArgs.length;
assert (myQueryName == null || ((theArgs != null ? theArgs.length : 0) == myParameters.size())) : "Wrong number of arguments: " + theArgs;
Map<String, List<String>> args = new LinkedHashMap<String, List<String>>();
for (int idx = 0; idx < theArgs.length; idx++) {
Object object = theArgs[idx];
IParameter nextParam = myParameters.get(idx);
if (myQueryName != null) {
args.put(Constants.PARAM_QUERY, Collections.singletonList(myQueryName));
}
if (object == null) {
if (nextParam.isRequired()) {
throw new NullPointerException("SearchParameter '" + nextParam.getName() + "' is required and may not be null");
}
} else {
List<List<String>> value = nextParam.encode(object);
ArrayList<String> paramValues = new ArrayList<String>(value.size());
args.put(nextParam.getName(), paramValues);
if (theArgs != null) {
for (int idx = 0; idx < theArgs.length; idx++) {
Object object = theArgs[idx];
IParameter nextParam = myParameters.get(idx);
for (List<String> nextParamEntry : value) {
StringBuilder b = new StringBuilder();
for (String str : nextParamEntry) {
if (b.length() > 0) {
b.append(",");
}
b.append(str.replace(",", "\\,"));
if (object == null) {
if (nextParam.isRequired()) {
throw new NullPointerException("SearchParameter '" + nextParam.getName() + "' is required and may not be null");
}
paramValues.add(b.toString());
}
}
} else {
List<List<String>> value = nextParam.encode(object);
ArrayList<String> paramValues = new ArrayList<String>(value.size());
args.put(nextParam.getName(), paramValues);
for (List<String> nextParamEntry : value) {
StringBuilder b = new StringBuilder();
for (String str : nextParamEntry) {
if (b.length() > 0) {
b.append(",");
}
b.append(str.replace(",", "\\,"));
}
paramValues.add(b.toString());
}
}
}
}
return new GetClientInvocation(args, getResourceName());
}
@Override
public List<IResource> invokeServer(IResourceProvider theResourceProvider, IdDt theId, IdDt theVersionId, Map<String, String[]> parameterValues) throws InvalidRequestException, InternalErrorException {
public List<IResource> invokeServer(IResourceProvider theResourceProvider, IdDt theId, IdDt theVersionId, Map<String, String[]> parameterValues) throws InvalidRequestException,
InternalErrorException {
assert theId == null;
assert theVersionId == null;
@ -109,23 +119,23 @@ public class SearchMethodBinding extends BaseMethodBinding {
}
continue;
}
List<List<String>> paramList=new ArrayList<List<String>>(value.length);
List<List<String>> paramList = new ArrayList<List<String>>(value.length);
for (String nextParam : value) {
if (nextParam.contains(",")==false) {
if (nextParam.contains(",") == false) {
paramList.add(Collections.singletonList(nextParam));
}else {
} else {
paramList.add(QueryUtil.splitQueryStringByCommasIgnoreEscape(nextParam));
}
}
params[i] = param.parse(paramList);
}
Object response;
try {
response = this.method.invoke(theResourceProvider, params);
response = this.myMethod.invoke(theResourceProvider, params);
} catch (IllegalAccessException e) {
throw new InternalErrorException(e);
} catch (IllegalArgumentException e) {
@ -141,7 +151,7 @@ public class SearchMethodBinding extends BaseMethodBinding {
@Override
public boolean matches(Request theRequest) {
if (!theRequest.getResourceName().equals(getResourceName())) {
ourLog.trace("Method {} doesn't match because resource name {} != {}", method.getName(), theRequest.getResourceName(), getResourceName());
ourLog.trace("Method {} doesn't match because resource name {} != {}", myMethod.getName(), theRequest.getResourceName(), getResourceName());
return false;
}
if (theRequest.getId() != null || theRequest.getVersion() != null) {
@ -161,20 +171,35 @@ public class SearchMethodBinding extends BaseMethodBinding {
for (int i = 0; i < this.myParameters.size(); i++) {
IParameter temp = this.myParameters.get(i);
methodParamsTemp.add(temp.getName());
if (temp.isRequired() && !theRequest.getParameterNames().contains(temp.getName())) {
ourLog.trace("Method {} doesn't match param '{}' is not present", method.getName(), temp.getName());
if (temp.isRequired() && !theRequest.getParameterNames().containsKey(temp.getName())) {
ourLog.trace("Method {} doesn't match param '{}' is not present", myMethod.getName(), temp.getName());
return false;
}
}
boolean retVal = methodParamsTemp.containsAll(theRequest.getParameterNames());
if (myQueryName != null) {
String[] queryNameValues = theRequest.getParameterNames().get(Constants.PARAM_QUERY);
if (queryNameValues != null && StringUtils.isNotBlank(queryNameValues[0])) {
String queryName = queryNameValues[0];
if (!myQueryName.equals(queryName)) {
ourLog.trace("Query name does not match {}", myQueryName);
return false;
} else {
methodParamsTemp.add(Constants.PARAM_QUERY);
}
} else {
ourLog.trace("Query name does not match {}", myQueryName);
return false;
}
}
boolean retVal = methodParamsTemp.containsAll(theRequest.getParameterNames().keySet());
ourLog.trace("Method {} matches: {}", method.getName(), retVal);
ourLog.trace("Method {} matches: {}", myMethod.getName(), retVal);
return retVal;
}
public void setMethod(Method method) {
this.method = method;
this.myMethod = method;
}
public void setParameters(List<IParameter> parameters) {

View File

@ -18,6 +18,11 @@ public class Constants {
public static final Map<String, EncodingUtil> FORMAT_VAL_TO_ENCODING;
public static final String CT_XML = "application/xml";
public static final String CT_JSON = "application/json";
public static final String CT_HTML = "text/html";
public static final String PARAM_NARRATIVE = "_narrative";
public static final String PARAM_HISTORY = "_history";
public static final String PARAM_PRETTY = "_pretty";
public static final String PARAM_QUERY = "_query";
static {
Map<String, EncodingUtil> valToEncoding = new HashMap<String, EncodingUtil>();

View File

@ -41,8 +41,6 @@ import ca.uhn.fhir.rest.server.provider.ServerProfileProvider;
public abstract class RestfulServer extends HttpServlet {
private static final String PARAM_HISTORY = "_history";
private static final String PARAM_PRETTY = "_pretty";
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulServer.class);
@ -164,7 +162,7 @@ public abstract class RestfulServer extends HttpServlet {
return myFhirContext;
}
private IParser getNewParser(EncodingUtil theResponseEncoding, boolean thePrettyPrint) {
private IParser getNewParser(EncodingUtil theResponseEncoding, boolean thePrettyPrint, NarrativeModeEnum theNarrativeMode) {
IParser parser;
switch (theResponseEncoding) {
case JSON:
@ -175,7 +173,7 @@ public abstract class RestfulServer extends HttpServlet {
parser = myFhirContext.newXmlParser();
break;
}
return parser.setPrettyPrint(thePrettyPrint);
return parser.setPrettyPrint(thePrettyPrint).setSuppressNarratives(theNarrativeMode == NarrativeModeEnum.SUPPRESS);
}
public Collection<ResourceBinding> getResourceBindings() {
@ -202,6 +200,16 @@ public abstract class RestfulServer extends HttpServlet {
return new ServerProfileProvider(getFhirContext());
}
public enum NarrativeModeEnum {
NORMAL,
SUPPRESS,
ONLY;
public static NarrativeModeEnum valueOfCaseInsensitive(String theCode) {
return valueOf(NarrativeModeEnum.class, theCode.toUpperCase());
}
}
protected void handleRequest(SearchMethodBinding.RequestType requestType, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
@ -260,7 +268,7 @@ public abstract class RestfulServer extends HttpServlet {
Map<String, String[]> params = new HashMap<String, String[]>(request.getParameterMap());
EncodingUtil responseEncoding = determineResponseEncoding(request, params);
String[] pretty = params.remove(PARAM_PRETTY);
String[] pretty = params.remove(Constants.PARAM_PRETTY);
boolean prettyPrint = false;
if (pretty != null && pretty.length > 0) {
if ("true".equals(pretty[0])) {
@ -268,6 +276,15 @@ public abstract class RestfulServer extends HttpServlet {
}
}
String[] narrative = params.remove(Constants.PARAM_NARRATIVE);
NarrativeModeEnum narrativeMode = null;
if (narrative != null && narrative.length > 0) {
narrativeMode = NarrativeModeEnum.valueOfCaseInsensitive(narrative[0]);
}
if (narrativeMode==null) {
narrativeMode = NarrativeModeEnum.NORMAL;
}
StringTokenizer tok = new StringTokenizer(requestPath, "/");
if (!tok.hasMoreTokens()) {
throw new MethodNotFoundException("No resource name specified");
@ -297,7 +314,7 @@ public abstract class RestfulServer extends HttpServlet {
if (tok.hasMoreTokens()) {
String nextString = tok.nextToken();
if (nextString.startsWith(PARAM_HISTORY)) {
if (nextString.startsWith(Constants.PARAM_HISTORY)) {
if (tok.hasMoreTokens()) {
versionId = new IdDt(tok.nextToken());
} else {
@ -313,7 +330,7 @@ public abstract class RestfulServer extends HttpServlet {
r.setId(id);
r.setVersion(versionId);
r.setOperation(operation);
r.setParameterNames(params.keySet());
r.setParameterNames(params);
r.setRequestType(requestType);
BaseMethodBinding resourceMethod = resourceBinding.getMethod(r);
@ -324,7 +341,7 @@ public abstract class RestfulServer extends HttpServlet {
List<IResource> result = resourceMethod.invokeServer(resourceBinding.getResourceProvider(), id, versionId, params);
switch (resourceMethod.getReturnType()) {
case BUNDLE:
streamResponseAsBundle(response, result, responseEncoding, fhirServerBase, completeUrl, prettyPrint, requestIsBrowser);
streamResponseAsBundle(response, result, responseEncoding, fhirServerBase, completeUrl, prettyPrint, requestIsBrowser, narrativeMode);
break;
case RESOURCE:
if (result.size() == 0) {
@ -332,7 +349,7 @@ public abstract class RestfulServer extends HttpServlet {
} else if (result.size() > 1) {
throw new InternalErrorException("Method returned multiple resources");
}
streamResponseAsResource(response, result.get(0), responseEncoding, prettyPrint, requestIsBrowser);
streamResponseAsResource(response, result.get(0), responseEncoding, prettyPrint, requestIsBrowser, narrativeMode);
break;
}
// resourceMethod.get
@ -409,13 +426,15 @@ public abstract class RestfulServer extends HttpServlet {
}
private void streamResponseAsBundle(HttpServletResponse theHttpResponse, List<IResource> theResult, EncodingUtil theResponseEncoding, String theServerBase, String theCompleteUrl,
boolean thePrettyPrint, boolean theRequestIsBrowser) throws IOException {
boolean thePrettyPrint, boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode) throws IOException {
assert !theServerBase.endsWith("/");
theHttpResponse.setStatus(200);
if (theRequestIsBrowser && myUseBrowserFriendlyContentTypes) {
theHttpResponse.setContentType(theResponseEncoding.getBrowserFriendlyBundleContentType());
} else if (theNarrativeMode == NarrativeModeEnum.ONLY) {
theHttpResponse.setContentType(Constants.CT_HTML);
} else {
theHttpResponse.setContentType(theResponseEncoding.getBundleContentType());
}
@ -449,7 +468,7 @@ public abstract class RestfulServer extends HttpServlet {
b.append(next.getId().getValue());
boolean haveQ = false;
if (thePrettyPrint) {
b.append('?').append(PARAM_PRETTY).append("=true");
b.append('?').append(Constants.PARAM_PRETTY).append("=true");
haveQ = true;
}
if (theResponseEncoding == EncodingUtil.JSON) {
@ -461,6 +480,9 @@ public abstract class RestfulServer extends HttpServlet {
}
b.append(Constants.PARAM_FORMAT).append("=json");
}
if (theNarrativeMode != NarrativeModeEnum.NORMAL) {
b.append(Constants.PARAM_NARRATIVE).append("=").append(theNarrativeMode.name().toLowerCase());
}
entry.getLinkSelf().setValue(b.toString());
}
}
@ -469,26 +491,41 @@ public abstract class RestfulServer extends HttpServlet {
PrintWriter writer = theHttpResponse.getWriter();
try {
getNewParser(theResponseEncoding, thePrettyPrint).encodeBundleToWriter(bundle, writer);
} finally {
if (theNarrativeMode == NarrativeModeEnum.ONLY) {
for (IResource next : theResult) {
writer.append(next.getText().getDiv().getValueAsString());
writer.append("<hr/>");
}
} else {
getNewParser(theResponseEncoding, thePrettyPrint, theNarrativeMode).encodeBundleToWriter(bundle, writer);
} } finally {
writer.close();
}
}
private void streamResponseAsResource(HttpServletResponse theHttpResponse, IResource theResource, EncodingUtil theResponseEncoding, boolean thePrettyPrint, boolean theRequestIsBrowser)
private void streamResponseAsResource(HttpServletResponse theHttpResponse, IResource theResource, EncodingUtil theResponseEncoding, boolean thePrettyPrint, boolean theRequestIsBrowser, NarrativeModeEnum theNarrativeMode)
throws IOException {
theHttpResponse.setStatus(200);
if (theRequestIsBrowser && myUseBrowserFriendlyContentTypes) {
theHttpResponse.setContentType(theResponseEncoding.getBrowserFriendlyBundleContentType());
} else if (theNarrativeMode == NarrativeModeEnum.ONLY) {
theHttpResponse.setContentType(Constants.CT_HTML);
} else {
theHttpResponse.setContentType(theResponseEncoding.getBundleContentType());
theHttpResponse.setContentType(theResponseEncoding.getResourceContentType());
}
theHttpResponse.setCharacterEncoding("UTF-8");
PrintWriter writer = theHttpResponse.getWriter();
getNewParser(theResponseEncoding, thePrettyPrint).encodeResourceToWriter(theResource, writer);
try {
if (theNarrativeMode == NarrativeModeEnum.ONLY) {
writer.append(theResource.getText().getDiv().getValueAsString());
} else {
getNewParser(theResponseEncoding, thePrettyPrint, theNarrativeMode).encodeResourceToWriter(theResource, writer);
}
} finally {
writer.close();
}
}

View File

@ -0,0 +1,7 @@
<div th:switch="${resource.precision.name()}">
<th:block th:case="YEAR" th:text="${#dates.format(resource.value,'yyyy')}">22 March 2012</th:block>
<th:block th:case="MONTH" th:text="${#dates.format(resource.value,'MMMM yyyy')}"></th:block>
<th:block th:case="DAY" th:text="${#dates.format(resource.value,'dd MMMM yyyy')}"></th:block>
<th:block th:case="SECOND" th:text="${#dates.format(resource.value,'dd MMMM yyyy HH:mm:ss')}"></th:block>
<th:block th:case="MILLI" th:text="${#dates.format(resource.value,'dd MMMM yyyy HH:mm:ss')}"></th:block>
</div>

View File

@ -19,6 +19,23 @@
<!--/*--> Complete Blood Count <!--*/-->
</div>
<table class="hapiPropertyTable">
<tbody>
<tr th:if="${not resource.status.empty}">
<td>Status</td>
<td th:text="${resource.status.value}">Final</td>
</tr>
<tr th:if="${not resource.issued.empty}">
<td>Issued</td>
<td th:narrative="${resource.issued}">22 March 2012</td>
</tr>
<tr th:if="${not resource.conclusion.empty}">
<td>Conclusion</td>
<td th:narrative="${resource.conclusion}">This is the conclusion text</td>
</tr>
</tbody>
</table>
<table class="hapiTableOfValues" th:if="${not #lists.isEmpty(resource.result)} and ${resource.result[0].resource != null}">
<thead>
<tr>
@ -53,13 +70,15 @@
</td>
<td th:text="${result.resource.status.value}">final</td>
</tr>
<tr th:class="${rowStat.odd}? 'hapiTableOfValuesRowOdd' : 'hapiTableOfValuesRowEven'" class="hapiTableOfValuesRowOdd">
<tr th:if="${not result.resource.comments.empty}" th:class="${rowStat.odd}? 'hapiTableOfValuesRowOdd' : 'hapiTableOfValuesRowEven'" class="hapiTableOfValuesRowOdd">
<td th:text="${result.resource.comments.value}" colspan="5">This is a comment</td>
</tr>
<!--/*-->
<!--/* The following are just examples that don't appear in the generated narrative -->
<tr class="hapiTableOfValuesRowEven"><td>Lkc</td><td>198 g/L</td><td>H</td><td>78 g/L - 99 g/L</td><td>final</td></tr>
<tr class="hapiTableOfValuesRowEven"><td colspan="5">Yet another comment</td></tr>
<!--*/-->
</th:block>
</tbody>
</table>

View File

@ -1,9 +1,19 @@
datetime.dtclass=ca.uhn.fhir.model.primitive.DateTimeDt
datetime.dtnarrative=classpath:ca/uhn/fhir/narrative/DateTimeDt.html
diagnosticreport.profile=http://hl7.org/fhir/profiles/DiagnosticReport
diagnosticreport.narrative=classpath:ca/uhn/fhir/narrative/DiagnosticReport.html
# Instant uses DateTime narrative
instant.dtclass=ca.uhn.fhir.model.primitive.InstantDt
instant.dtnarrative=classpath:ca/uhn/fhir/narrative/DateTimeDt.html
patient.profile=http://hl7.org/fhir/profiles/Patient
patient.narrative=classpath:ca/uhn/fhir/narrative/Patient.html
quantity.dtclass=ca.uhn.fhir.model.dstu.composite.QuantityDt
quantity.dtnarrative=classpath:ca/uhn/fhir/narrative/QuantityDt.html
quantity.dtnarrative=classpath:ca/uhn/fhir/narrative/QuantityDt.html
string.dtclass=ca.uhn.fhir.model.primitive.StringDt
string.dtnarrative=classpath:ca/uhn/fhir/narrative/StringDt.html

View File

@ -11,11 +11,13 @@ public class ExampleRestfulClient {
//START SNIPPET: client
public static void main(String[] args) {
FhirContext ctx = new FhirContext(Patient.class);
FhirContext ctx = new FhirContext();
String serverBase = "http://foo.com/fhirServerBase";
// Create the client
RestfulClientImpl client = ctx.newRestfulClient(RestfulClientImpl.class, serverBase);
// The client is now ready for use!
// Try the client out! This method will invoke the server
List<Patient> patients = client.getPatient(new StringDt("SMITH"));
}

View File

@ -54,8 +54,14 @@ public class MyPatient extends Patient {
/********
* Accessors and mutators follow
*
* IMPORTANT:
* Each extension is required to have an getter/accessor and a stter/mutator.
* You are highly recommended to create getters which create instances if they
* do not already exist, since this is how the rest of the HAPI FHIR API works.
********/
/** Getter for important dates */
public List<DateTimeDt> getImportantDates() {
if (myImportantDates==null) {
myImportantDates = new ArrayList<DateTimeDt>();
@ -63,14 +69,20 @@ public class MyPatient extends Patient {
return myImportantDates;
}
/** Getter for pet name */
public StringDt getPetName() {
if (myPetName == null) {
myPetName = new StringDt();
}
return myPetName;
}
/** Setter for important dates */
public void setImportantDates(List<DateTimeDt> theImportantDates) {
myImportantDates = theImportantDates;
}
/** Setter for pet name */
public void setPetName(StringDt thePetName) {
myPetName = thePetName;
}

View File

@ -67,6 +67,15 @@ public List<Patient> searchByLastName(@Required(name=Patient.SP_FAMILY) StringDt
}
//END SNIPPET: searchStringParam
//START SNIPPET: searchNamedQuery
@Search(queryName="namedQuery1")
public List<Patient> searchByNamedQuery(@Required(name="someparam") StringDt theSomeParam) {
List<Patient> retVal = new ArrayList<Patient>();
// ...populate...
return retVal;
}
//END SNIPPET: searchNamedQuery
//START SNIPPET: searchIdentifierParam
@Search()
public List<Patient> searchByIdentifier(@Required(name=Patient.SP_IDENTIFIER) IdentifierDt theId) {

View File

@ -228,6 +228,19 @@
<code>http://fhir.example.com/Observation?name=urn:fakenames|123,urn:fakenames|456</code>
</p>
<h4>AND Relationship Query Parameters</h4>
<p>
To accept a composite parameter, use a parameter type which implements the
<a href="./apidocs/ca/uhn/fhir/model/api/IQueryParameterAnd.html">IQueryParameterAnd</a>
interface.
</p>
<p>
See the section below on <a href="#DATE_RANGES">date ranges</a> for
one example of an AND parameter.
</p>
</subsection>
<subsection name="Date Parameters - Simple">
@ -270,6 +283,7 @@
</subsection>
<a name="DATE_RANGES"/>
<subsection name="Date Parameters - Ranges">
<p>
@ -335,6 +349,31 @@
</subsection>
<subsection name="Named Queries (_query)">
<p>
FHIR supports <a href="http://www.hl7.org/implement/standards/fhir/search.html#advanced">named queries</a>,
which may have specific behaviour defined. The following example shows how to create a Search
operation with a name.
</p>
<p>
This operation can only be invoked by explicitly specifying the given query name
in the request URL. Note that the query does not need to take any parameters.
</p>
<macro name="snippet">
<param name="id" value="searchNamedQuery" />
<param name="file" value="src/site/example/java/example/RestfulPatientResourceProviderMore.java" />
</macro>
<p>
Example URL to invoke this method:<br/>
<code>http://fhir.example.com/Patient?_query=namedQuery1&amp;someparam=value</code>
</p>
</subsection>
</section>

View File

@ -2,6 +2,8 @@ package ca.uhn.fhir.context;
import java.util.List;
import javax.naming.OperationNotSupportedException;
import ca.uhn.fhir.model.api.IElement;
import ca.uhn.fhir.model.api.IExtension;
import ca.uhn.fhir.model.api.IResource;
@ -11,6 +13,7 @@ import ca.uhn.fhir.model.api.annotation.Extension;
import ca.uhn.fhir.model.api.annotation.ResourceDef;
import ca.uhn.fhir.model.dstu.composite.ContainedDt;
import ca.uhn.fhir.model.dstu.composite.IdentifierDt;
import ca.uhn.fhir.model.dstu.composite.NarrativeDt;
import ca.uhn.fhir.model.primitive.DateDt;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.model.primitive.StringDt;
@ -232,8 +235,12 @@ public class ResourceWithExtensionsA implements IResource {
@Override
public ContainedDt getContained() {
// TODO Auto-generated method stub
return null;
throw new IllegalStateException();
}
@Override
public NarrativeDt getText() {
throw new IllegalStateException();
}
}

View File

@ -17,7 +17,9 @@ import ca.uhn.fhir.model.dstu.composite.ResourceReferenceDt;
import ca.uhn.fhir.model.dstu.resource.DiagnosticReport;
import ca.uhn.fhir.model.dstu.resource.Observation;
import ca.uhn.fhir.model.dstu.resource.Patient;
import ca.uhn.fhir.model.dstu.valueset.DiagnosticReportStatusEnum;
import ca.uhn.fhir.model.dstu.valueset.ObservationStatusEnum;
import ca.uhn.fhir.model.primitive.StringDt;
import ca.uhn.fhir.parser.DataFormatException;
public class ThymeleafNarrativeGeneratorTest {
@ -30,18 +32,18 @@ public class ThymeleafNarrativeGeneratorTest {
gen.setIgnoreFailures(false);
gen.setIgnoreMissingTemplates(true);
}
@Test
public void testGeneratePatient() throws DataFormatException {
Patient value = new Patient();
value.addIdentifier().setSystem("urn:names").setValue("123456");
value.addName().addFamily("blow").addGiven("joe").addGiven("john");
value.getAddressFirstRep().addLine("123 Fake Street").addLine("Unit 1");
value.getAddressFirstRep().setCity("Toronto").setState("ON").setCountry("Canada");
value.setBirthDate(new Date(), TemporalPrecisionEnum.DAY);
String output = gen.generateNarrative("http://hl7.org/fhir/profiles/Patient", value).getDiv().getValueAsString();
ourLog.info(output);
@ -64,32 +66,41 @@ public class ThymeleafNarrativeGeneratorTest {
@Test
public void testGenerateDiagnosticReportWithObservations() throws DataFormatException, IOException {
DiagnosticReport value = new DiagnosticReport();
value.getName().setText("Some Diagnostic Report");
Observation obs = new Observation();
obs.getName().addCoding().setCode("1938HB").setDisplay("Hemoglobin");
obs.setValue(new QuantityDt(null, 2.223, "mg/L"));
obs.addReferenceRange().setLow(new QuantityDt(2.20)).setHigh(new QuantityDt(2.99));
obs.setStatus(ObservationStatusEnum.FINAL);
obs.setComments("This is a result comment");
ResourceReferenceDt result = value.addResult();
result.setResource(obs);
value.getIssued().setValueAsString("2011-02-22T11:13:00");
value.setStatus(DiagnosticReportStatusEnum.FINAL);
value.getName().setText("Some Diagnostic Report");
{
Observation obs = new Observation();
obs.getName().addCoding().setCode("1938HB").setDisplay("Hemoglobin");
obs.setValue(new QuantityDt(null, 2.223, "mg/L"));
obs.addReferenceRange().setLow(new QuantityDt(2.20)).setHigh(new QuantityDt(2.99));
obs.setStatus(ObservationStatusEnum.FINAL);
obs.setComments("This is a result comment");
ResourceReferenceDt result = value.addResult();
result.setResource(obs);
}
{
Observation obs = new Observation();
obs.setValue(new StringDt("HELLO!"));
value.addResult().setResource(obs);
}
NarrativeDt generateNarrative = gen.generateNarrative("http://hl7.org/fhir/profiles/DiagnosticReport", value);
String output = generateNarrative.getDiv().getValueAsString();
ourLog.info(output);
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\">Some Diagnostic Report</div>"));
// Now try it with the parser
FhirContext context = new FhirContext();
context.setNarrativeGenerator(gen);
output = context.newXmlParser().setPrettyPrint(true).encodeResourceToString(value);
ourLog.info(output);
assertThat(output, StringContains.containsString("<div class=\"hapiHeaderText\">Some Diagnostic Report</div>"));
}
}

View File

@ -64,8 +64,9 @@ public class JsonParserTest {
public void testNarrativeGeneration() throws DataFormatException, IOException {
Patient patient = new Patient();
patient.addName().addFamily("Smith");
Organization org = new Organization();
patient.getManagingOrganization().setResource(org);
INarrativeGenerator gen = mock(INarrativeGenerator.class);
XhtmlDt xhtmlDt = new XhtmlDt("<div>help</div>");

View File

@ -268,6 +268,42 @@ public class ClientTest {
}
@Test
public void testSearchNamedQueryOneParam() throws Exception {
String msg = getPatientFeedWithOneResult();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(httpClient.execute(capt.capture())).thenReturn(httpResponse);
when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
ITestClient client = ctx.newRestfulClient(ITestClient.class, "http://foo");
client.getPatientOneParam(new StringDt("BB"));
assertEquals("http://foo/Patient?_query=someQueryOneParam&param1=BB", capt.getValue().getURI().toString());
}
@Test
public void testSearchNamedQueryNoParams() throws Exception {
String msg = getPatientFeedWithOneResult();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(httpClient.execute(capt.capture())).thenReturn(httpResponse);
when(httpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(httpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML + "; charset=UTF-8"));
when(httpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(msg), Charset.forName("UTF-8")));
ITestClient client = ctx.newRestfulClient(ITestClient.class, "http://foo");
client.getPatientNoParams();
assertEquals("http://foo/Patient?_query=someQueryNoParams", capt.getValue().getURI().toString());
}
private String getPatientFeedWithOneResult() {
//@formatter:off
String msg = "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n" +

View File

@ -44,4 +44,10 @@ public interface ITestClient extends IBasicClient {
@Search()
public Patient getPatientWithIncludes(@Required(name = "withIncludes") StringDt theString, @Include List<PathSpecification> theIncludes);
@Search(queryName="someQueryNoParams")
public Patient getPatientNoParams();
@Search(queryName="someQueryOneParam")
public Patient getPatientOneParam(@Required(name="param1") StringDt theParam);
}

View File

@ -546,6 +546,47 @@ public class ResfulServerMethodTest {
}
@Test
public void testSearchNamedNoParams() throws Exception {
// HttpPost httpPost = new HttpPost("http://localhost:" + ourPort +
// "/Patient/1");
// httpPost.setEntity(new StringEntity("test",
// ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/?_query=someQueryNoParams");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
Patient patient = (Patient) ourCtx.newXmlParser().parseBundle(responseContent).getEntries().get(0).getResource();
assertEquals("someQueryNoParams", patient.getName().get(1).getFamilyAsSingleString());
}
@Test
public void testSearchNamedOneParam() throws Exception {
// HttpPost httpPost = new HttpPost("http://localhost:" + ourPort +
// "/Patient/1");
// httpPost.setEntity(new StringEntity("test",
// ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/?_query=someQueryOneParam&param1=AAAA");
HttpResponse status = ourClient.execute(httpGet);
String responseContent = IOUtils.toString(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode());
Patient patient = (Patient) ourCtx.newXmlParser().parseBundle(responseContent).getEntries().get(0).getResource();
assertEquals("AAAA", patient.getName().get(1).getFamilyAsSingleString());
}
public static class DummyRestfulServer extends RestfulServer {
private static final long serialVersionUID = 1L;
@ -604,6 +645,20 @@ public class ResfulServerMethodTest {
return idToPatient;
}
@Search(queryName="someQueryNoParams")
public Patient getPatientNoParams() {
Patient next = getIdToPatient().get("1");
next.addName().addFamily("someQueryNoParams");
return next;
}
@Search(queryName="someQueryOneParam")
public Patient getPatientOneParam(@Required(name="param1") StringDt theParam) {
Patient next = getIdToPatient().get("1");
next.addName().addFamily(theParam.getValue());
return next;
}
@Search()
public Patient getPatientWithIncludes(@Required(name = "withIncludes") StringDt theString,
@Include(allow= {"include1","include2", "include3"}) List<PathSpecification> theIncludes) {

View File

@ -24,7 +24,7 @@ public class ResourceMethodTest {
@Before
public void before() throws NoSuchMethodException, SecurityException {
rm = new SearchMethodBinding(MethodReturnTypeEnum.RESOURCE, Patient.class, ResourceMethodTest.class.getMethod("before"));
rm = new SearchMethodBinding(MethodReturnTypeEnum.RESOURCE, Patient.class, ResourceMethodTest.class.getMethod("before"), null);
}
@Test