diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildAny.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildAny.java index f3b2beea93a..7df3d093d7e 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildAny.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/RuntimeChildAny.java @@ -60,9 +60,11 @@ public class RuntimeChildAny extends RuntimeChildChoiceDefinition { if (o1res && o2res) { return theO1.getSimpleName().compareTo(theO2.getSimpleName()); } else if (o1res) { - return 1; - }else { return -1; + } else if (o1res == false && o2res == false) { + return 0; + }else { + return 1; } }}); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Bundle.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Bundle.java index ddda2ee9ff5..8ad3c33a293 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Bundle.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/Bundle.java @@ -221,7 +221,12 @@ public class Bundle extends BaseBundle /* implements IElement */{ RuntimeResourceDefinition def = theContext.getResourceDefinition(theResource); if (theResource.getId() != null && StringUtils.isNotBlank(theResource.getId().getValue())) { - entry.getTitle().setValue(def.getName() + " " + theResource.getId().getValue()); + String title = ResourceMetadataKeyEnum.TITLE.get(theResource); + if (title != null) { + entry.getTitle().setValue(title); + } else { + entry.getTitle().setValue(def.getName() + " " + theResource.getId().getValue()); + } StringBuilder b = new StringBuilder(); b.append(theServerBase); diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/ResourceMetadataKeyEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/ResourceMetadataKeyEnum.java index b445058651c..14f164fcf92 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/ResourceMetadataKeyEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/api/ResourceMetadataKeyEnum.java @@ -25,6 +25,8 @@ import static org.apache.commons.lang3.StringUtils.*; import java.util.Date; import java.util.Map; +import org.apache.commons.lang3.StringUtils; + import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; @@ -71,7 +73,7 @@ public abstract class ResourceMetadataKeyEnum { theResource.getResourceMetadata().put(PREVIOUS_ID, theObject); } }; - + /** * The value for this key is the bundle entry Published time. This is * defined by FHIR as "Time resource copied into the feed", which is @@ -128,6 +130,26 @@ public abstract class ResourceMetadataKeyEnum { theResource.getResourceMetadata().put(TAG_LIST, theObject); } }; + + /** + * If present and populated with a string (as an instance of {@link String}), + * this value contains the title for this resource, as supplied in any bundles containing the + * resource. + *

+ * Values for this key are of type {@link String} + *

+ */ + public static final ResourceMetadataKeyEnum TITLE = new ResourceMetadataKeyEnum("TITLE") { + @Override + public String get(IResource theResource) { + return getStringFromMetadataOrNullIfNone(theResource.getResourceMetadata(), TITLE); + } + + @Override + public void put(IResource theResource, String theObject) { + theResource.getResourceMetadata().put(TITLE, theObject); + } + }; /** @@ -182,7 +204,6 @@ public abstract class ResourceMetadataKeyEnum { } - @Override public boolean equals(Object obj) { if (this == obj) @@ -200,6 +221,8 @@ public abstract class ResourceMetadataKeyEnum { return true; } + + public abstract T get(IResource theResource); @Override @@ -210,6 +233,10 @@ public abstract class ResourceMetadataKeyEnum { return result; } + private String name() { + return myValue; + } + public abstract void put(IResource theResource, T theObject); @Override @@ -217,32 +244,28 @@ public abstract class ResourceMetadataKeyEnum { return myValue; } - private String name() { - return myValue; + private static IdDt getIdFromMetadataOrNullIfNone(Map, Object> theResourceMetadata, ResourceMetadataKeyEnum theKey) { + Object retValObj = theResourceMetadata.get(theKey); + if (retValObj == null) { + return null; + } else if (retValObj instanceof String) { + if (isNotBlank((String) retValObj)) { + return new IdDt((String) retValObj); + } else { + return null; + } + } else if (retValObj instanceof IdDt) { + if (((IdDt) retValObj).isEmpty()) { + return null; + } else { + return (IdDt) retValObj; + } + } else if (retValObj instanceof Number) { + return new IdDt(((Number)retValObj).toString()); + } + throw new InternalErrorException("Found an object of type '" + retValObj.getClass().getCanonicalName() + "' in resource metadata for key " + theKey.name() + " - Expected " + IdDt.class.getCanonicalName()); } - private static IdDt getIdFromMetadataOrNullIfNone(Map, Object> theResourceMetadata, ResourceMetadataKeyEnum theKey) { - Object retValObj = theResourceMetadata.get(theKey); - if (retValObj == null) { - return null; - } else if (retValObj instanceof String) { - if (isNotBlank((String) retValObj)) { - return new IdDt((String) retValObj); - } else { - return null; - } - } else if (retValObj instanceof IdDt) { - if (((IdDt) retValObj).isEmpty()) { - return null; - } else { - return (IdDt) retValObj; - } - } else if (retValObj instanceof Number) { - return new IdDt(((Number)retValObj).toString()); - } - throw new InternalErrorException("Found an object of type '" + retValObj.getClass().getCanonicalName() + "' in resource metadata for key " + theKey.name() + " - Expected " + IdDt.class.getCanonicalName()); - } - private static InstantDt getInstantFromMetadataOrNullIfNone(Map, Object> theResourceMetadata, ResourceMetadataKeyEnum theKey) { Object retValObj = theResourceMetadata.get(theKey); if (retValObj == null) { @@ -259,4 +282,18 @@ public abstract class ResourceMetadataKeyEnum { throw new InternalErrorException("Found an object of type '" + retValObj.getClass().getCanonicalName() + "' in resource metadata for key " + theKey.name() + " - Expected " + InstantDt.class.getCanonicalName()); } + private static String getStringFromMetadataOrNullIfNone(Map, Object> theResourceMetadata, ResourceMetadataKeyEnum theKey) { + Object retValObj = theResourceMetadata.get(theKey); + if (retValObj == null) { + return null; + } else if (retValObj instanceof String) { + if (StringUtils.isBlank(((String) retValObj))) { + return null; + } else { + return (String) retValObj; + } + } + throw new InternalErrorException("Found an object of type '" + retValObj.getClass().getCanonicalName() + "' in resource metadata for key " + theKey.name() + " - Expected " + String.class.getCanonicalName()); + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/composite/CodingDt.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/composite/CodingDt.java index c4947031186..368f42e1aa2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/composite/CodingDt.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/composite/CodingDt.java @@ -407,9 +407,11 @@ public class CodingDt @Override public String getValueAsQueryToken() { if (org.apache.commons.lang3.StringUtils.isNotBlank(getSystem().getValueAsString())) { - return getSystem().getValueAsString() + '|' + getCode().getValueAsString(); - } else { + return getSystem().getValueAsString() + '|' + getCode().getValueAsString(); + } else if (getSystem().getValue()==null) { return getCode().getValueAsString(); + } else { + return '|' + getCode().getValueAsString(); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/composite/IdentifierDt.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/composite/IdentifierDt.java index e4f0e90eb1d..de4811ab332 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/composite/IdentifierDt.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/model/dstu/composite/IdentifierDt.java @@ -413,7 +413,9 @@ public class IdentifierDt @Override public String getValueAsQueryToken() { if (org.apache.commons.lang3.StringUtils.isNotBlank(getSystem().getValueAsString())) { - return getSystem().getValueAsString() + '|' + getValue().getValueAsString(); + return getSystem().getValueAsString() + '|' + getValue().getValueAsString(); + } else if (getSystem().getValue() == null) { + return getValue().getValueAsString(); } else { return '|' + getValue().getValueAsString(); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java index 2b3de6fdac6..d2622224797 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/BaseThymeleafNarrativeGenerator.java @@ -69,17 +69,100 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener private boolean myIgnoreMissingTemplates = true; private TemplateEngine myProfileTemplateEngine; - private HashMap myProfileToNarrativeName; - private HashMap, String> myClassToNarrativeName; + private TemplateEngine myTitleTemplateEngine; + private HashMap myProfileToName; + private HashMap, String> myClassToName; private HashMap myNameToNarrativeTemplate; - private boolean myApplyDefaultDatatypeTemplates=true; + private boolean myApplyDefaultDatatypeTemplates = true; private volatile boolean myInitialized; + private HashMap myNameToTitleTemplate; + @Override public NarrativeDt generateNarrative(IResource theResource) { return generateNarrative(null, theResource); } + @Override + public String generateTitle(IResource theResource) { + return generateTitle(null, theResource); + } + + @Override + public String generateTitle(String theProfile, IResource theResource) { + if (!myInitialized) { + initialize(); + } + + String name = null; + if (StringUtils.isNotBlank(theProfile)) { + name = myProfileToName.get(theProfile); + } + if (name == null) { + name = myClassToName.get(theResource.getClass()); + } + + if (name == null) { + if (myIgnoreMissingTemplates) { + ourLog.debug("No title template available for profile: {}", theProfile); + return null; + } else { + throw new DataFormatException("No title template for class " + theResource.getClass().getCanonicalName()); + } + } + + try { + Context context = new Context(); + context.setVariable("resource", theResource); + + String result = myTitleTemplateEngine.process(name, context); + StringBuilder b = new StringBuilder(); + boolean inTag = false; + for (int i = 0; i < result.length(); i++) { + char nextChar = result.charAt(i); + char prevChar = i > 0 ? result.charAt(i - 1) : '\n'; + if (nextChar == '<') { + inTag = true; + continue; + } else if (inTag) { + if (nextChar == '>') { + inTag = false; + } + continue; + } else if (nextChar <= ' ') { + if (prevChar <= ' ' || prevChar == '>') { + continue; + } else { + b.append(' '); + } + } else { + b.append(nextChar); + } + } + + while (b.length() > 0 && b.charAt(b.length()-1) == ' ') { + b.setLength(b.length() - 1); + } + + result = b.toString(); + if (result.startsWith("<") && result.contains(">")) { + result = result.substring(result.indexOf('>') + 1); + } + if (result.endsWith(">") && result.contains("<")) { + result = result.substring(0, result.lastIndexOf('<')); + } + + return result; + } catch (Exception e) { + if (myIgnoreFailures) { + ourLog.error("Failed to generate narrative", e); + return "No title available - Error: " + e.getMessage(); + } else { + throw new DataFormatException(e); + } + } + } + @Override public NarrativeDt generateNarrative(String theProfile, IResource theResource) { if (!myInitialized) { @@ -88,10 +171,10 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener String name = null; if (StringUtils.isNotBlank(theProfile)) { - name = myProfileToNarrativeName.get(theProfile); + name = myProfileToName.get(theProfile); } if (name == null) { - name = myClassToNarrativeName.get(theResource.getClass()); + name = myClassToName.get(theResource.getClass()); } if (name == null) { @@ -106,7 +189,7 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener try { Context context = new Context(); context.setVariable("resource", theResource); - + String result = myProfileTemplateEngine.process(name, context); if (myCleanWhitespace) { @@ -131,9 +214,10 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener if (myInitialized) { return; } - myProfileToNarrativeName = new HashMap(); - myClassToNarrativeName = new HashMap, String>(); + myProfileToName = new HashMap(); + myClassToName = new HashMap, String>(); myNameToNarrativeTemplate = new HashMap(); + myNameToTitleTemplate = new HashMap(); List propFileName = getPropertyFile(); @@ -160,6 +244,15 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener myProfileTemplateEngine.setDialect(dialect); myProfileTemplateEngine.initialize(); } + { + myTitleTemplateEngine = new TemplateEngine(); + TemplateResolver resolver = new TemplateResolver(); + resolver.setResourceResolver(new TitleResourceResolver()); + myTitleTemplateEngine.setTemplateResolver(resolver); + StandardDialect dialect = new StandardDialect(); + myTitleTemplateEngine.setDialect(dialect); + myTitleTemplateEngine.initialize(); + } myInitialized = true; } @@ -167,12 +260,9 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener protected abstract List getPropertyFile(); /** - * If set to true (which is the default), most whitespace will - * be trimmed from the generated narrative before it is returned. + * If set to true (which is the default), most whitespace will be trimmed from the generated narrative before it is returned. *

- * Note that in order to preserve formatting, not all whitespace is trimmed. - * Repeated whitespace characters (e.g. "\n \n ") will be - * trimmed to a single space. + * Note that in order to preserve formatting, not all whitespace is trimmed. Repeated whitespace characters (e.g. "\n \n ") will be trimmed to a single space. *

*/ public boolean isCleanWhitespace() { @@ -180,30 +270,24 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener } /** - * If set to true, which is the default, if any failure occurs - * during narrative generation the generator will suppress any generated - * exceptions, and simply return a default narrative indicating that no - * narrative is available. + * If set to true, which is the default, if any failure occurs during narrative generation the generator will suppress any generated exceptions, and simply return a default narrative + * indicating that no narrative is available. */ public boolean isIgnoreFailures() { return myIgnoreFailures; } /** - * If set to true, will return an empty narrative block for any profiles - * where no template is available + * If set to true, will return an empty narrative block for any profiles where no template is available */ public boolean isIgnoreMissingTemplates() { return myIgnoreMissingTemplates; } /** - * If set to true (which is the default), most whitespace will - * be trimmed from the generated narrative before it is returned. + * If set to true (which is the default), most whitespace will be trimmed from the generated narrative before it is returned. *

- * Note that in order to preserve formatting, not all whitespace is trimmed. - * Repeated whitespace characters (e.g. "\n \n ") will be - * trimmed to a single space. + * Note that in order to preserve formatting, not all whitespace is trimmed. Repeated whitespace characters (e.g. "\n \n ") will be trimmed to a single space. *

*/ public void setCleanWhitespace(boolean theCleanWhitespace) { @@ -211,18 +295,15 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener } /** - * If set to true, which is the default, if any failure occurs - * during narrative generation the generator will suppress any generated - * exceptions, and simply return a default narrative indicating that no - * narrative is available. + * If set to true, which is the default, if any failure occurs during narrative generation the generator will suppress any generated exceptions, and simply return a default narrative + * indicating that no narrative is available. */ public void setIgnoreFailures(boolean theIgnoreFailures) { myIgnoreFailures = theIgnoreFailures; } /** - * If set to true, will return an empty narrative block for any profiles - * where no template is available + * If set to true, will return an empty narrative block for any profiles where no template is available */ public void setIgnoreMissingTemplates(boolean theIgnoreMissingTemplates) { myIgnoreMissingTemplates = theIgnoreMissingTemplates; @@ -230,7 +311,7 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener private void loadProperties(String propFileName) throws IOException { ourLog.debug("Loading narrative properties file: {}", propFileName); - + Properties file = new Properties(); InputStream resource = loadResource(propFileName); @@ -245,13 +326,22 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener String narrativePropName = name + ".narrative"; String narrativeName = file.getProperty(narrativePropName); - if (isBlank(narrativeName)) { - throw new ConfigurationException("Found property '" + nextKey + "' but no corresponding property '" + narrativePropName + "' in file " + propFileName); + String titlePropName = name + ".title"; + String titleName = file.getProperty(titlePropName); + if (isBlank(narrativeName) && isBlank(titleName)) { + throw new ConfigurationException("Found property '" + nextKey + "' but no corresponding property '" + narrativePropName + "' or '" + titlePropName + "' in file " + propFileName); } - String narrative = IOUtils.toString(loadResource(narrativeName)); - myProfileToNarrativeName.put(file.getProperty(nextKey), name); - myNameToNarrativeTemplate.put(name, narrative); + myProfileToName.put(file.getProperty(nextKey), name); + + if (StringUtils.isNotBlank(narrativeName)) { + String narrative = IOUtils.toString(loadResource(narrativeName)); + myNameToNarrativeTemplate.put(name, narrative); + } + if (StringUtils.isNotBlank(titleName)) { + String title = IOUtils.toString(loadResource(titleName)); + myNameToTitleTemplate.put(name, title); + } } else if (nextKey.endsWith(".class")) { @@ -262,9 +352,9 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener String className = file.getProperty(nextKey); - Class dtClass; + Class clazz; try { - dtClass = Class.forName(className); + clazz = Class.forName(className); } catch (ClassNotFoundException e) { ourLog.warn("Unknown datatype class '{}' identified in narrative file {}", name, propFileName); continue; @@ -272,16 +362,27 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener String narrativePropName = name + ".narrative"; String narrativeName = file.getProperty(narrativePropName); - if (isBlank(narrativeName)) { - throw new ConfigurationException("Found property '" + nextKey + "' but no corresponding property '" + narrativePropName + "' in file " + propFileName); + String titlePropName = name + ".title"; + String titleName = file.getProperty(titlePropName); + if (isBlank(narrativeName) && isBlank(titleName)) { + throw new ConfigurationException("Found property '" + nextKey + "' but no corresponding property '" + narrativePropName + "' or '" + titlePropName + "' in file " + propFileName); } - String narrative = IOUtils.toString(loadResource(narrativeName)); - myClassToNarrativeName.put(dtClass, name); - myNameToNarrativeTemplate.put(name, narrative); + myClassToName.put(clazz, name); + + if (StringUtils.isNotBlank(narrativeName)) { + String narrative = IOUtils.toString(loadResource(narrativeName)); + myNameToNarrativeTemplate.put(name, narrative); + } + if (StringUtils.isNotBlank(titleName)) { + String title = IOUtils.toString(loadResource(titleName)); + myNameToTitleTemplate.put(name, title); + } } else if (nextKey.endsWith(".narrative")) { continue; + } else if (nextKey.endsWith(".title")) { + continue; } else { throw new ConfigurationException("Invalid property name: " + nextKey); } @@ -379,14 +480,13 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener final IStandardExpression expression = expressionParser.parseExpression(configuration, theArguments, attributeValue); final Object value = expression.execute(configuration, theArguments); - theElement.removeAttribute(theAttributeName); theElement.clearChildren(); Context context = new Context(); context.setVariable("resource", value); - - String name = myClassToNarrativeName.get(value.getClass()); + + String name = myClassToName.get(value.getClass()); if (name == null) { if (myIgnoreMissingTemplates) { ourLog.debug("No narrative template available for type: {}", value.getClass()); @@ -438,4 +538,21 @@ public abstract class BaseThymeleafNarrativeGenerator implements INarrativeGener return new ReaderInputStream(new StringReader(template)); } } + + private final class TitleResourceResolver implements IResourceResolver { + @Override + public String getName() { + return getClass().getCanonicalName(); + } + + @Override + public InputStream getResourceAsStream(TemplateProcessingParameters theTemplateProcessingParameters, String theName) { + String template = myNameToTitleTemplate.get(theName); + if (template == null) { + ourLog.info("No narative template for resource profile: {}", theName); + return new ReaderInputStream(new StringReader("")); + } + return new ReaderInputStream(new StringReader(template)); + } + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGenerator.java index 28ce4355fac..75faf958d1f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGenerator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGenerator.java @@ -64,4 +64,6 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe return myUseHapiServerConformanceNarrative; } + + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/INarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/INarrativeGenerator.java index 80060c1042d..8ac9ec3d64a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/INarrativeGenerator.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/INarrativeGenerator.java @@ -30,4 +30,8 @@ public interface INarrativeGenerator { NarrativeDt generateNarrative(IResource theResource); + String generateTitle(IResource theResource); + + String generateTitle(String theProfile, IResource theResource); + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java index 8e98094a7a6..a584fa07ba6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/JsonParser.java @@ -222,7 +222,7 @@ public class JsonParser extends BaseParser implements IParser { eventWriter.writeEnd(); // entry array eventWriter.writeEnd(); - eventWriter.close(); + eventWriter.flush(); } private void encodeChildElementToStreamWriter(RuntimeResourceDefinition theResDef, IResource theResource, JsonGenerator theWriter, IElement theValue, BaseRuntimeElementDefinition theChildDef, String theChildName) throws IOException { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java index 6d25614122d..2fd3c1824c6 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/ParserState.java @@ -417,6 +417,9 @@ class ParserState { if (myEntry.getUpdated().isEmpty() == false) { ResourceMetadataKeyEnum.UPDATED.put(myEntry.getResource(), myEntry.getUpdated()); } + + ResourceMetadataKeyEnum.TITLE.put(myEntry.getResource(), myEntry.getTitle().getValue()); + if (myEntry.getCategories().isEmpty() == false) { TagList tagList = new TagList(); for (Tag next : myEntry.getCategories()) { @@ -608,7 +611,6 @@ class ParserState { myPreResourceState = thePreResourceState; } - @SuppressWarnings("unused") public void attributeValue(String theName, String theValue) throws DataFormatException { // ignore by default } @@ -617,7 +619,6 @@ class ParserState { // ignore by default } - @SuppressWarnings("unused") public void enteringNewElement(String theNamespaceURI, String theLocalPart) throws DataFormatException { // ignore by default } @@ -657,7 +658,7 @@ class ParserState { myStack = theState; } - public void string(@SuppressWarnings("unused") String theData) { + public void string(String theData) { // ignore by default } @@ -665,7 +666,7 @@ class ParserState { // allow an implementor to override } - public void xmlEvent(@SuppressWarnings("unused") XMLEvent theNextEvent) { + public void xmlEvent(XMLEvent theNextEvent) { // ignore } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IdentifierListParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IdentifierListParam.java index 46103f4d90e..e0d8c047481 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IdentifierListParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/IdentifierListParam.java @@ -31,7 +31,7 @@ import ca.uhn.fhir.rest.method.QualifiedParamList; public class IdentifierListParam implements IQueryParameterOr { private List myIdentifiers = new ArrayList(); - + /** * Returns all identifiers associated with this list */ @@ -64,4 +64,10 @@ public class IdentifierListParam implements IQueryParameterOr { } } + public void addIdentifier(IdentifierDt theIdentifierDt) { + if (theIdentifierDt != null && theIdentifierDt.isEmpty() == false) { + getIdentifiers().add(theIdentifierDt); + } + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/SearchParameter.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/SearchParameter.java index 2546a9af512..ca12a833c98 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/SearchParameter.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/param/SearchParameter.java @@ -160,6 +160,8 @@ public class SearchParameter extends BaseQueryParameter { myParamType = SearchParamTypeEnum.QUANTITY; } else if (ReferenceParam.class.isAssignableFrom(type)) { myParamType = SearchParamTypeEnum.REFERENCE; + } else if (IdentifierListParam.class.isAssignableFrom(type)) { + myParamType = SearchParamTypeEnum.TOKEN; } else { throw new ConfigurationException("Unknown search parameter type: " + type); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java index 99a6527f2d3..f74b0c11afd 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/RestfulServer.java @@ -20,11 +20,10 @@ package ca.uhn.fhir.rest.server; * #L% */ -import static org.apache.commons.lang3.StringUtils.*; +import static org.apache.commons.lang3.StringUtils.isNotBlank; import java.io.IOException; import java.io.OutputStreamWriter; -import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.lang.reflect.Method; @@ -46,8 +45,9 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.lang3.Validate; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.exception.ExceptionUtils; import ca.uhn.fhir.context.FhirContext; @@ -542,22 +542,26 @@ public class RestfulServer extends HttpServlet { theResponse.setContentType("text/plain"); theResponse.setCharacterEncoding("UTF-8"); theResponse.getWriter().write(e.getMessage()); + } catch (Throwable e) { - int statusCode = 500; - if (e instanceof InternalErrorException) { - ourLog.error("Failure during REST processing", e); - } else if (e instanceof BaseServerResponseException) { - ourLog.warn("Failure during REST processing: {}", e.toString()); - statusCode=((BaseServerResponseException) e).getStatusCode(); - } else { - ourLog.warn("Failure during REST processing: {}", e.toString()); - } - OperationOutcome oo = new OperationOutcome(); Issue issue = oo.addIssue(); issue.getSeverity().setValueAsEnum(IssueSeverityEnum.ERROR); - issue.getDetails().setValue(e.toString() + "\n\n" + ExceptionUtils.getStackTrace(e)); + + int statusCode = 500; + if (e instanceof InternalErrorException) { + ourLog.error("Failure during REST processing", e); + issue.getDetails().setValue(e.toString() + "\n\n" + ExceptionUtils.getStackTrace(e)); + } else if (e instanceof BaseServerResponseException) { + ourLog.warn("Failure during REST processing: {}", e.toString()); + statusCode=((BaseServerResponseException) e).getStatusCode(); + issue.getDetails().setValue(e.getMessage()); + } else { + ourLog.error("Failure during REST processing: {}"+ e.toString(), e); + issue.getDetails().setValue(e.toString() + "\n\n" + ExceptionUtils.getStackTrace(e)); + } + streamResponseAsResource(this, theResponse, oo, determineResponseEncoding(theRequest), true, false, NarrativeModeEnum.NORMAL, statusCode,false); @@ -776,6 +780,14 @@ public class RestfulServer extends HttpServlet { bundle.getLinkSelf().setValue(theCompleteUrl); for (IResource next : theResult) { + + if (theContext.getNarrativeGenerator() != null) { + String title = theContext.getNarrativeGenerator().generateTitle(next); + if (StringUtils.isNotBlank(title)) { + ResourceMetadataKeyEnum.TITLE.put(next, title); + } + } + bundle.addResource(next, theContext, theServerBase); } diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/narratives.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/narratives.properties index c28153ec1ea..0062367beb8 100644 --- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/narratives.properties +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/narratives.properties @@ -32,6 +32,9 @@ quantity.narrative=classpath:ca/uhn/fhir/narrative/QuantityDt.html patient.class=ca.uhn.fhir.model.dstu.resource.Patient patient.narrative=classpath:ca/uhn/fhir/narrative/Patient.html +patient.title=classpath:ca/uhn/fhir/narrative/title/Patient.html + diagnosticreport.class=ca.uhn.fhir.model.dstu.resource.DiagnosticReport -diagnosticreport.narrative=classpath:ca/uhn/fhir/narrative/DiagnosticReport.html \ No newline at end of file +diagnosticreport.narrative=classpath:ca/uhn/fhir/narrative/DiagnosticReport.html +diagnosticreport.title=classpath:ca/uhn/fhir/narrative/title/DiagnosticReport.html diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/title/DiagnosticReport.html b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/title/DiagnosticReport.html new file mode 100644 index 00000000000..733c936c5a7 --- /dev/null +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/title/DiagnosticReport.html @@ -0,0 +1,9 @@ +
+ + + + + + + +
\ No newline at end of file diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/title/Patient.html b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/title/Patient.html new file mode 100644 index 00000000000..6b94c7e9760 --- /dev/null +++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/title/Patient.html @@ -0,0 +1,9 @@ +
+ Dr + John + SMITH + Jr + + (8708660) + +
diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/model/primitive/CodingDtTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/model/primitive/CodingDtTest.java new file mode 100644 index 00000000000..9f637a6b4ea --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/model/primitive/CodingDtTest.java @@ -0,0 +1,41 @@ +package ca.uhn.fhir.model.primitive; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import ca.uhn.fhir.model.dstu.composite.CodingDt; + +public class CodingDtTest { + + @Test + public void testTokenWithPipeInValue() { + CodingDt dt = new CodingDt(); + dt.setValueAsQueryToken(null, "a|b|c"); + + assertEquals("a", dt.getSystem().getValueAsString()); + assertEquals("b|c", dt.getCode().getValue()); + assertEquals("a|b|c", dt.getValueAsQueryToken()); + } + + @Test + public void testTokenWithPipeInValueAndNoSystem() { + CodingDt dt = new CodingDt(); + dt.setValueAsQueryToken(null, "|b|c"); + + assertEquals("", dt.getSystem().getValueAsString()); + assertEquals("b|c", dt.getCode().getValue()); + assertEquals("|b|c", dt.getValueAsQueryToken()); + } + + @Test + public void testTokenNoSystem() { + CodingDt dt = new CodingDt(); + dt.setValueAsQueryToken(null, "c"); + + assertEquals(null, dt.getSystem().getValueAsString()); + assertEquals("c", dt.getCode().getValue()); + assertEquals("c", dt.getValueAsQueryToken()); + } + +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/model/primitive/IdentifierDtTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/model/primitive/IdentifierDtTest.java index b2465f5f1e5..32347442f1f 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/model/primitive/IdentifierDtTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/model/primitive/IdentifierDtTest.java @@ -28,4 +28,14 @@ public class IdentifierDtTest { assertEquals("|b|c", dt.getValueAsQueryToken()); } + @Test + public void testTokenNoSystem() { + IdentifierDt dt = new IdentifierDt(); + dt.setValueAsQueryToken(null, "c"); + + assertEquals(null, dt.getSystem().getValueAsString()); + assertEquals("c", dt.getValue().getValue()); + assertEquals("c", dt.getValueAsQueryToken()); + } + } diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorTest.java index 0e03ceea7d8..e2e0ef0bb6a 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/narrative/DefaultThymeleafNarrativeGeneratorTest.java @@ -46,9 +46,12 @@ public class DefaultThymeleafNarrativeGeneratorTest { value.setBirthDate(new Date(), TemporalPrecisionEnum.DAY); - String output = gen.generateNarrative("http://hl7.org/fhir/profiles/Patient", value).getDiv().getValueAsString(); - - ourLog.info(output); + String output = gen.generateNarrative(value).getDiv().getValueAsString(); + assertThat(output, StringContains.containsString("
joe john BLOW
")); + + String title = gen.generateTitle(value); + assertEquals("joe john BLOW (123456)", title); + ourLog.info(title); } @Test @@ -105,6 +108,11 @@ public class DefaultThymeleafNarrativeGeneratorTest { ourLog.info(output); assertThat(output, StringContains.containsString("
Some Diagnostic Report
")); + String title = gen.generateTitle(value); + ourLog.info(title); + assertEquals("Some Diagnostic Report - final - 2 observations", title); + + // Now try it with the parser FhirContext context = new FhirContext(); diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ExceptionTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ExceptionTest.java index 824a4bc3571..6e891af6674 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ExceptionTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/ExceptionTest.java @@ -43,28 +43,44 @@ public class ExceptionTest { private static RestfulServer servlet; @Test - public void testSearchNormalMatch() throws Exception { + public void testInternalError() throws Exception { { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwInternalError=aaa"); HttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info(responseContent); assertEquals(500, status.getStatusLine().getStatusCode()); OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent); assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("InternalErrorException: Exception Text")); } + } + @Test + public void testInternalErrorFormatted() throws Exception { { - HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwInternalError=aaa&_format=json"); + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwInternalError=aaa&_format=true"); HttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info(responseContent); assertEquals(500, status.getStatusLine().getStatusCode()); - OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newJsonParser().parseResource(responseContent); + OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newXmlParser().parseResource(responseContent); assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("InternalErrorException: Exception Text")); } } + @Test + public void testInternalErrorJson() throws Exception { + HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?throwInternalError=aaa&_format=json"); + HttpResponse status = ourClient.execute(httpGet); + String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); + ourLog.info(responseContent); + assertEquals(500, status.getStatusLine().getStatusCode()); + OperationOutcome oo = (OperationOutcome) servlet.getFhirContext().newJsonParser().parseResource(responseContent); + assertThat(oo.getIssueFirstRep().getDetails().getValue(), StringContains.containsString("InternalErrorException: Exception Text")); + } @AfterClass diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/SearchTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/SearchTest.java index 03fa904a2b2..56fea2d6202 100644 --- a/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/SearchTest.java +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/rest/server/SearchTest.java @@ -23,9 +23,8 @@ import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.dstu.resource.Patient; -import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.rest.annotation.OptionalParam; -import ca.uhn.fhir.rest.annotation.RequiredParam; import ca.uhn.fhir.rest.annotation.Search; import ca.uhn.fhir.rest.param.StringParam; import ca.uhn.fhir.testutil.RandomServerPortProvider; @@ -36,7 +35,6 @@ import ca.uhn.fhir.testutil.RandomServerPortProvider; public class SearchTest { private static CloseableHttpClient ourClient; - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchTest.class); private static int ourPort; private static Server ourServer; private static FhirContext ourCtx = new FhirContext(); @@ -46,12 +44,14 @@ public class SearchTest { HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_id=aaa"); HttpResponse status = ourClient.execute(httpGet); String responseContent = IOUtils.toString(status.getEntity().getContent()); + IOUtils.closeQuietly(status.getEntity().getContent()); assertEquals(200, status.getStatusLine().getStatusCode()); Bundle bundle = ourCtx.newXmlParser().parseBundle(responseContent); assertEquals(1, bundle.getEntries().size()); Patient p = bundle.getResources(Patient.class).get(0); assertEquals("idaaa", p.getNameFirstRep().getFamilyAsSingleString()); + assertEquals("IDAAA (identifier123)", bundle.getEntries().get(0).getTitle().getValue()); } @AfterClass @@ -68,6 +68,8 @@ public class SearchTest { ServletHandler proxyHandler = new ServletHandler(); RestfulServer servlet = new RestfulServer(); + servlet.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator()); + servlet.setResourceProviders(patientProvider); ServletHolder servletHolder = new ServletHolder(servlet); proxyHandler.addServletWithMapping(servletHolder, "/*"); @@ -92,6 +94,7 @@ public class SearchTest { Patient patient = new Patient(); patient.setId("1"); + patient.addIdentifier("system", "identifier123"); patient.addName().addFamily("id"+theParam.getValue()); retVal.add(patient); return retVal; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java index de1e75b5462..da2af0ad0f0 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseFhirDao.java @@ -747,6 +747,9 @@ public abstract class BaseFhirDao { } } + String title = ResourceMetadataKeyEnum.TITLE.get(theResource); + theEntity.setTitle(title); + } protected ResourceTable toEntity(IResource theResource) { @@ -784,6 +787,10 @@ public abstract class BaseFhirDao { retVal.getResourceMetadata().put(ResourceMetadataKeyEnum.PUBLISHED, theEntity.getPublished()); retVal.getResourceMetadata().put(ResourceMetadataKeyEnum.UPDATED, theEntity.getUpdated()); + if (theEntity.getTitle()!=null) { + ResourceMetadataKeyEnum.TITLE.put(retVal, theEntity.getTitle()); + } + if (theEntity.getDeleted()!=null) { ResourceMetadataKeyEnum.DELETED_AT.put(retVal, new InstantDt(theEntity.getDeleted())); } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDao.java index 341f92c2547..9ffa64e8bf4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/FhirResourceDao.java @@ -431,7 +431,10 @@ public class FhirResourceDao extends BaseFhirDao implements ArrayList singleCodePredicates = (new ArrayList()); if (StringUtils.isNotBlank(system)) { singleCodePredicates.add(builder.equal(from.get("mySystem"), system)); + } else if (system == null) { + // don't check the system } else { + // If the system is "", we only match on null systems singleCodePredicates.add(builder.isNull(from.get("mySystem"))); } if (StringUtils.isNotBlank(code)) { diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java index e8b8c090b1d..97d58dcdcb4 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchParameterMap.java @@ -35,6 +35,17 @@ public class SearchParameterMap extends HashMap>()); + } + + get(theName).add(theOr.getValuesAsQueryTokens()); + } + public void add(String theName, IQueryParameterType theParam) { if (theParam == null) { return; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java index af13ee05c8d..314200ea5c6 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/BaseHasResource.java @@ -17,6 +17,8 @@ import ca.uhn.fhir.model.primitive.InstantDt; @MappedSuperclass public abstract class BaseHasResource { + private static final int MAX_TITLE_LENGTH = 100; + @Column(name = "RES_DELETED_AT", nullable = true) @Temporal(TemporalType.TIMESTAMP) private Date myDeleted; @@ -33,6 +35,9 @@ public abstract class BaseHasResource { @Lob() private byte[] myResource; + @Column(name = "RES_TITLE", nullable = true, length = MAX_TITLE_LENGTH) + private String myTitle; + @Temporal(TemporalType.TIMESTAMP) @Column(name = "RES_UPDATED", nullable = false) private Date myUpdated; @@ -61,6 +66,10 @@ public abstract class BaseHasResource { public abstract Collection getTags(); + public String getTitle() { + return myTitle; + } + public InstantDt getUpdated() { return new InstantDt(myUpdated); } @@ -87,6 +96,10 @@ public abstract class BaseHasResource { myResource = theResource; } + public void setTitle(String theTitle) { + myTitle = theTitle; + } + public void setUpdated(Date theUpdated) { myUpdated = theUpdated; } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java index 1b0d40c58d8..3f11b2e1d8f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceIndexedSearchParamToken.java @@ -8,7 +8,10 @@ import org.apache.commons.lang3.StringUtils; @Entity @Table(name = "HFJ_SPIDX_TOKEN" /* , indexes = { @Index(name = "IDX_SP_TOKEN", columnList = "SP_SYSTEM,SP_VALUE") } */) -@org.hibernate.annotations.Table(appliesTo = "HFJ_SPIDX_TOKEN", indexes = { @org.hibernate.annotations.Index(name = "IDX_SP_TOKEN", columnNames = { "RES_TYPE", "SP_NAME", "SP_SYSTEM", "SP_VALUE" }) }) +@org.hibernate.annotations.Table(appliesTo = "HFJ_SPIDX_TOKEN", indexes = { + @org.hibernate.annotations.Index(name = "IDX_SP_TOKEN", columnNames = { "RES_TYPE", "SP_NAME", "SP_SYSTEM", "SP_VALUE" }), + @org.hibernate.annotations.Index(name = "IDX_SP_TOKEN_UNQUAL", columnNames = { "RES_TYPE", "SP_NAME", "SP_VALUE" }) +}) public class ResourceIndexedSearchParamToken extends BaseResourceIndexedSearchParam { private static final long serialVersionUID = 1L; diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java index a89a945b133..2787e1ba957 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/entity/ResourceTable.java @@ -252,6 +252,7 @@ public class ResourceTable extends BaseHasResource implements Serializable { retVal.setResourceType(myResourceType); retVal.setVersion(myVersion); + retVal.setTitle(getTitle()); retVal.setPublished(getPublished()); retVal.setUpdated(getUpdated()); retVal.setEncoding(getEncoding()); diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaConformanceProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaConformanceProvider.java index 55ff548e03d..bc5bef6062f 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaConformanceProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/provider/JpaConformanceProvider.java @@ -7,11 +7,13 @@ import ca.uhn.fhir.model.dstu.resource.Conformance; import ca.uhn.fhir.model.dstu.resource.Conformance.Rest; import ca.uhn.fhir.model.dstu.resource.Conformance.RestResource; import ca.uhn.fhir.model.primitive.DecimalDt; +import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.provider.ServerConformanceProvider; public class JpaConformanceProvider extends ServerConformanceProvider { + private String myImplementationDescription; private IFhirSystemDao mySystemDao; public JpaConformanceProvider(RestfulServer theRestfulServer, IFhirSystemDao theSystemDao) { @@ -19,24 +21,29 @@ public class JpaConformanceProvider extends ServerConformanceProvider { mySystemDao = theSystemDao; super.setCache(false); } - - + @Override public Conformance getServerConformance() { - + Map counts = mySystemDao.getResourceCounts(); - + Conformance retVal = super.getServerConformance(); for (Rest nextRest : retVal.getRest()) { for (RestResource nextResource : nextRest.getResource()) { Long count = counts.get(nextResource.getType().getValueAsString()); - if (count!=null) { + if (count != null) { nextResource.addUndeclaredExtension(false, "http://hl7api.sourceforge.net/hapi-fhir/res/extdefs.html#resourceCount", new DecimalDt(count)); } } } - + + retVal.getImplementation().setDescription(myImplementationDescription); + return retVal; } + public void setImplementationDescription(String theImplDesc) { + myImplementationDescription = theImplDesc; + } + } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoTest.java index 1781e4d0e2d..3f0e193d86c 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/FhirResourceDaoTest.java @@ -39,7 +39,9 @@ import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.StringDt; import ca.uhn.fhir.rest.api.MethodOutcome; import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; +import ca.uhn.fhir.rest.gclient.IQuery; import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.IdentifierListParam; import ca.uhn.fhir.rest.param.QualifiedDateParam; import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.StringParam; @@ -122,6 +124,50 @@ public class FhirResourceDaoTest { assertEquals(o1id.toUnqualifiedVersionless(), p1.getManagingOrganization().getReference().toUnqualifiedVersionless()); } + @Test + public void testSearchTokenParam() { + Patient patient = new Patient(); + patient.addIdentifier("urn:system", "testSearchTokenParam001"); + patient.addName().addFamily("Tester").addGiven("testSearchTokenParam1"); + ourPatientDao.create(patient); + + patient = new Patient(); + patient.addIdentifier("urn:system", "testSearchTokenParam002"); + patient.addName().addFamily("Tester").addGiven("testSearchTokenParam2"); + ourPatientDao.create(patient); + + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_IDENTIFIER, new IdentifierDt("urn:system", "testSearchTokenParam001")); + IBundleProvider retrieved = ourPatientDao.search(map); + assertEquals(1, retrieved.size()); + } + { + SearchParameterMap map = new SearchParameterMap(); + map.add(Patient.SP_IDENTIFIER, new IdentifierDt(null, "testSearchTokenParam001")); + IBundleProvider retrieved = ourPatientDao.search(map); + assertEquals(1, retrieved.size()); + } + { + SearchParameterMap map = new SearchParameterMap(); + IdentifierListParam listParam = new IdentifierListParam(); + listParam.addIdentifier(new IdentifierDt("urn:system", "testSearchTokenParam001")); + listParam.addIdentifier(new IdentifierDt("urn:system", "testSearchTokenParam002")); + map.add(Patient.SP_IDENTIFIER, listParam); + IBundleProvider retrieved = ourPatientDao.search(map); + assertEquals(2, retrieved.size()); + } + { + SearchParameterMap map = new SearchParameterMap(); + IdentifierListParam listParam = new IdentifierListParam(); + listParam.addIdentifier(new IdentifierDt(null, "testSearchTokenParam001")); + listParam.addIdentifier(new IdentifierDt("urn:system", "testSearchTokenParam002")); + map.add(Patient.SP_IDENTIFIER, listParam); + IBundleProvider retrieved = ourPatientDao.search(map); + assertEquals(2, retrieved.size()); + } + } + @Test public void testIdParam() { Patient patient = new Patient(); @@ -253,13 +299,12 @@ public class FhirResourceDaoTest { @Test public void testSearchWithNoResults() { - IBundleProvider value = ourObservationDao.search(new SearchParameterMap()); + IBundleProvider value = ourDeviceDao.search(new SearchParameterMap()); + for (IResource next : value.getResources(0, value.size())) { + ourDeviceDao.delete(next.getId()); + } - /* - * This may fail at some point, which means another test has probably added a device - * resource. This test depends on there being none, so if that happens this test - * should be refactored to use another resource type - */ + value = ourDeviceDao.search(new SearchParameterMap()); assertEquals(0, value.size()); List res = value.getResources(0, 0); @@ -361,6 +406,7 @@ public class FhirResourceDaoTest { Patient patient = new Patient(); patient.addIdentifier("urn:system", "001"); patient.addName().addFamily("testSearchNameParam01Fam").addGiven("testSearchNameParam01Giv"); + ResourceMetadataKeyEnum.TITLE.put(patient, "P1TITLE"); id1 = ourPatientDao.create(patient).getId(); } { @@ -375,6 +421,7 @@ public class FhirResourceDaoTest { List patients = toList(ourPatientDao.search(params)); assertEquals(1, patients.size()); assertEquals(id1.getIdPart(), patients.get(0).getId().getIdPart()); + assertEquals("P1TITLE", ResourceMetadataKeyEnum.TITLE.get(patients.get(0))); // Given name shouldn't return for family param params = new HashMap(); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml index 985efbb2952..dc413dc9e05 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/pom.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/pom.xml @@ -148,6 +148,31 @@ 1.7 + + + org.eclipse.m2e + lifecycle-mapping + 1.0.0 + + + + + + + + [0.4,) + + + + + + + + + + + + diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java index 269693e9bcd..47a4c874045 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java @@ -55,7 +55,10 @@ public class TestRestfulServer extends RestfulServer { JpaSystemProvider sp = new JpaSystemProvider(systemDao); setPlainProviders(sp); + String implDesc = getInitParameter("ImplementationDescription"); + JpaConformanceProvider confProvider = new JpaConformanceProvider(this, systemDao); + confProvider.setImplementationDescription(implDesc); setServerConformanceProvider(confProvider); setUseBrowserFriendlyContentTypes(true); diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml index e57d46c69c1..56b0f86a471 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml @@ -31,6 +31,7 @@ + diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml.orig b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml.orig new file mode 100644 index 00000000000..33ba4ebc5cb --- /dev/null +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/hapi-fhir-server-database-config.xml.orig @@ -0,0 +1,56 @@ + + + + + + + + + + + + +<<<<<<< HEAD + +======= + +>>>>>>> ca0929df07b8d435a3b756f89998f68e2aae79fc + + + + + + + + + + + + + + + + + + + diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/templates/tmpl-home-welcome.html b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/templates/tmpl-home-welcome.html index 90ca83c831e..7e96c765396 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/templates/tmpl-home-welcome.html +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/templates/tmpl-home-welcome.html @@ -1,14 +1,31 @@
+ +

+ This is the home for the FHIR test server operated by + University Health Network. This server + (and the testing application you are currently using to access it) + is entirely built using + HAPI-FHIR, + a 100% open-source Java implementation of the + FHIR specification. +

+
+ +

+ You are accessing the public FHIR server + . This server is hosted elsewhere on the internet + but is being accessed using the HAPI client implementation. +

+

- This is the home for the FHIR test server operated by - University Health Network. -

-

- This is not a production server! + + + This is not a production server! + Do not store any information here that contains personal health information - or otherwise confidential information. This server will be regularly purged + or any other confidential information. This server will be regularly purged and reloaded with fixed test data.

diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/templates/tmpl-home-welcome.html.orig b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/templates/tmpl-home-welcome.html.orig new file mode 100644 index 00000000000..0cc1666b486 --- /dev/null +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/templates/tmpl-home-welcome.html.orig @@ -0,0 +1,75 @@ + + +

+ +

+ This is the home for the FHIR test server operated by + University Health Network. This server + (and the testing application you are currently using to access it) + is entirely built using + HAPI-FHIR, + a 100% open-source Java implementation of the + FHIR specification. +

+

+ Here are some things you might wish to try: +

+
    +
  • + View a + list of patients + on this server. +
  • +
  • + Construct a + search query + on this server. +
  • +
  • + Access a + different server + (use the Server menu at the top of the page to see a list of public FHIR servers) +
  • +
+
+ +

+ You are accessing the public FHIR server + . This server is hosted elsewhere on the internet + but is being accessed using +

+
+

+ + list of patients + on this server. + +

  • + Construct a + search query + on this server. +
  • +
  • + Access a + different server + (use the Server menu at the top of the page to see a list of public FHIR servers) +
  • + +======= +>>>>>>> ca0929df07b8d435a3b756f89998f68e2aae79fc +
    + diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml index 40a6bc71310..adbee6f7be4 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/web.xml @@ -35,6 +35,10 @@ fhirServlet ca.uhn.fhirtest.TestRestfulServer + + ImplementationDescription + UHN Test Server + 1 diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/xsd/javaee_6.xsd b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/xsd/javaee_6.xsd index b1ee0b83831..9fb587749ce 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/xsd/javaee_6.xsd +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/webapp/WEB-INF/xsd/javaee_6.xsd @@ -75,8 +75,6 @@ - - diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/test/java/ca/uhn/fhirtest/UhnFhirTestApp.java b/hapi-fhir-jpaserver-uhnfhirtest/src/test/java/ca/uhn/fhirtest/UhnFhirTestApp.java index 32c82ed9d20..f9be1f23344 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/test/java/ca/uhn/fhirtest/UhnFhirTestApp.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/test/java/ca/uhn/fhirtest/UhnFhirTestApp.java @@ -23,7 +23,7 @@ public class UhnFhirTestApp { public static void main(String[] args) throws Exception { // new File("target/testdb").mkdirs(); - System.setProperty("fhir.db.location", "target/testdb"); + System.setProperty("fhir.db.location", "/target/testdb"); int myPort = 8888; Server server = new Server(myPort); diff --git a/hapi-fhir-testpage-overlay/.settings/org.eclipse.wst.common.component b/hapi-fhir-testpage-overlay/.settings/org.eclipse.wst.common.component index d088b0783b8..d2052e8be95 100644 --- a/hapi-fhir-testpage-overlay/.settings/org.eclipse.wst.common.component +++ b/hapi-fhir-testpage-overlay/.settings/org.eclipse.wst.common.component @@ -3,7 +3,6 @@ - uses diff --git a/hapi-fhir-testpage-overlay/pom.xml b/hapi-fhir-testpage-overlay/pom.xml index 2108c28a8de..0dfc35e73f2 100644 --- a/hapi-fhir-testpage-overlay/pom.xml +++ b/hapi-fhir-testpage-overlay/pom.xml @@ -31,10 +31,10 @@ - javax.servlet - javax.servlet-api - 3.0.1 - provided + javax.servlet + javax.servlet-api + 3.1.0 + provided diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java index 1aa2e347b69..d12d48def68 100644 --- a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/Controller.java @@ -50,6 +50,7 @@ import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.EncodingEnum; import ca.uhn.fhir.to.model.HomeRequest; import ca.uhn.fhir.to.model.ResourceRequest; +import ca.uhn.fhir.to.model.TransactionRequest; @org.springframework.stereotype.Controller() public class Controller { @@ -67,6 +68,39 @@ public class Controller { @Autowired private TemplateEngine myTemplateEngine; + @RequestMapping(value = { "/transaction" }) + public String actionTransaction(final TransactionRequest theRequest, final BindingResult theBindingResult, final ModelMap theModel) { + addCommonParams(theRequest, theModel); + + GenericClient client = theRequest.newClient(myCtx, myConfig); + + String body = preProcessMessageBody(theRequest.getTransactionBody()); + + Bundle bundle; + try { + if (body.startsWith("{")) { + bundle = myCtx.newJsonParser().parseBundle(body); + } else if (body.startsWith("<")) { + bundle = myCtx.newXmlParser().parseBundle(body); + } else { + theModel.put("errorMsg", "Message body does not appear to be a valid FHIR resource instance document. Body should start with '<' (for XML encoding) or '{' (for JSON encoding)."); + return "home"; + } + } catch (DataFormatException e) { + ourLog.warn("Failed to parse bundle", e); + theModel.put("errorMsg", "Failed to parse transaction bundle body. Error was: " + e.getMessage()); + return "home"; + } + + long start = System.currentTimeMillis(); +// client.tr + long delay = System.currentTimeMillis() - start; + + processAndAddLastClientInvocation(client, ResultType.RESOURCE, theModel, delay, "Loaded conformance"); + + return "result"; + } + @RequestMapping(value = { "/conformance" }) public String actionConformance(final HomeRequest theRequest, final BindingResult theBindingResult, final ModelMap theModel) { addCommonParams(theRequest, theModel); @@ -427,18 +461,7 @@ public class Controller { return; } - body = body.trim(); - - StringBuilder b = new StringBuilder(); - for (int i = 0; i < body.length(); i++) { - char nextChar = body.charAt(i); - int nextCharI = nextChar; - if (nextCharI == 65533) { - continue; - } - b.append(nextChar); - } - body = b.toString(); + body = preProcessMessageBody(body); IResource resource; try { @@ -485,6 +508,25 @@ public class Controller { } + private String preProcessMessageBody(String theBody) { + if(theBody==null) { + return ""; + } + String retVal = theBody.trim(); + + StringBuilder b = new StringBuilder(); + for (int i = 0; i < retVal.length(); i++) { + char nextChar = retVal.charAt(i); + int nextCharI = nextChar; + if (nextCharI == 65533) { + continue; + } + b.append(nextChar); + } + retVal = b.toString(); + return retVal; + } + private void doActionHistory(HttpServletRequest theReq, HomeRequest theRequest, BindingResult theBindingResult, ModelMap theModel, String theMethod, String theMethodDescription) { addCommonParams(theRequest, theModel); diff --git a/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/model/TransactionRequest.java b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/model/TransactionRequest.java new file mode 100644 index 00000000000..759297cc6a5 --- /dev/null +++ b/hapi-fhir-testpage-overlay/src/main/java/ca/uhn/fhir/to/model/TransactionRequest.java @@ -0,0 +1,18 @@ +package ca.uhn.fhir.to.model; + +import org.springframework.web.bind.annotation.ModelAttribute; + +public class TransactionRequest extends HomeRequest { + + private String myTransactionBody; + + @ModelAttribute("transactionBody") + public String getTransactionBody() { + return myTransactionBody; + } + + public void setTransactionBody(String theTransactionBody) { + myTransactionBody = theTransactionBody; + } + +} diff --git a/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/hapi-fhir-tester-config.xml b/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/hapi-fhir-tester-config.xml index 5f57a04ea06..a7df352d042 100644 --- a/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/hapi-fhir-tester-config.xml +++ b/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/hapi-fhir-tester-config.xml @@ -11,6 +11,7 @@ + test, TEST, http://uhnvesb01d.uhn.on.ca:25180/uhn-fhir-service-1.2/ home , Localhost Server , http://localhost:8887/fhir/context hi , Health Intersections , http://fhir.healthintersections.com.au/open furore , Spark - Furore Reference Server , http://spark.furore.com/fhir @@ -21,12 +22,5 @@ - - - - - - - \ No newline at end of file diff --git a/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/.swp b/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/.swp deleted file mode 100644 index 14e1f4548dd..00000000000 Binary files a/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/.swp and /dev/null differ diff --git a/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/home.html b/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/home.html index 03c7f73c88f..47242ad2e2a 100644 --- a/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/home.html +++ b/hapi-fhir-testpage-overlay/src/main/webapp/WEB-INF/templates/home.html @@ -31,22 +31,22 @@ - + Server HAPI Restful Server + + Software + + - + + FHIR Base - - Software - - - - - @@ -67,7 +67,7 @@ Retrieve the server's conformance statement.
    -
    +
    + + +
    +
    + Post a bundle containing multiple resources to the server and + store all resources within a single atomic transaction. +
    +
    +
    + +
    +
    +
    +
    +
    + Bundle + * +
    + +
    +
    +
    + +
    +
    @@ -151,7 +206,7 @@ Show all of the tags currently in use on the server
    -
    +