No narrative available - Error: " + e.getMessage() + "
");
- } catch (Exception e1) {
- // last resort..
- }
- theNarrative.setStatusAsString("empty");
- return;
- }
- throw new DataFormatException(e);
- }
+ super.populateResourceNarrative(theResource);
+ return false;
}
protected abstract List getPropertyFile();
- private synchronized void initialize(final FhirContext theContext) {
+ private synchronized void initialize() {
if (myInitialized) {
return;
}
- ourLog.info("Initializing narrative generator");
-
- myClassToName = new HashMap, String>();
- myNameToNarrativeTemplate = new HashMap();
-
List propFileName = getPropertyFile();
-
try {
- if (myApplyDefaultDatatypeTemplates) {
- loadProperties(DefaultThymeleafNarrativeGenerator.NARRATIVES_PROPERTIES);
- }
- for (String next : propFileName) {
- loadProperties(next);
- }
+ NarrativeTemplateManifest manifest = NarrativeTemplateManifest.forManifestFileLocation(getFhirContext(), propFileName);
+ setManifest(manifest);
} catch (IOException e) {
- ourLog.info("Failed to load property file " + propFileName, e);
- throw new ConfigurationException("Can not load property file " + propFileName, e);
- }
-
- {
- myProfileTemplateEngine = new TemplateEngine();
- ProfileResourceResolver resolver = new ProfileResourceResolver();
- myProfileTemplateEngine.setTemplateResolver(resolver);
- StandardDialect dialect = new StandardDialect() {
- @Override
- public Set getProcessors(String theDialectPrefix) {
- Set retVal = super.getProcessors(theDialectPrefix);
- retVal.add(new NarrativeAttributeProcessor(theContext, theDialectPrefix));
- return retVal;
- }
-
- };
- myProfileTemplateEngine.setDialect(dialect);
- if (this.resolver != null) {
- myProfileTemplateEngine.setMessageResolver(this.resolver);
- }
+ throw new InternalErrorException(e);
}
myInitialized = true;
}
- public void setMessageResolver(IMessageResolver resolver) {
- this.resolver = resolver;
- if (myProfileTemplateEngine != null && resolver != null) {
- myProfileTemplateEngine.setMessageResolver(resolver);
- }
- }
-
- /**
- * 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.
- *
- */
- public boolean isCleanWhitespace() {
- return myCleanWhitespace;
- }
-
- /**
- * 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
- */
- public boolean isIgnoreMissingTemplates() {
- return myIgnoreMissingTemplates;
- }
-
- private void loadProperties(String propFileName) throws IOException {
- ourLog.debug("Loading narrative properties file: {}", propFileName);
-
- Properties file = new Properties();
-
- InputStream resource = loadResource(propFileName);
- file.load(resource);
- for (Object nextKeyObj : file.keySet()) {
- String nextKey = (String) nextKeyObj;
- if (nextKey.endsWith(".profile")) {
- String name = nextKey.substring(0, nextKey.indexOf(".profile"));
- if (isBlank(name)) {
- continue;
- }
-
- String narrativePropName = name + ".narrative";
- String narrativeName = file.getProperty(narrativePropName);
- if (isBlank(narrativeName)) {
- //FIXME resource leak
- throw new ConfigurationException("Found property '" + nextKey + "' but no corresponding property '" + narrativePropName + "' in file " + propFileName);
- }
-
- if (StringUtils.isNotBlank(narrativeName)) {
- String narrative = IOUtils.toString(loadResource(narrativeName), Constants.CHARSET_UTF8);
- myNameToNarrativeTemplate.put(name, narrative);
- }
-
- } else if (nextKey.endsWith(".class")) {
-
- String name = nextKey.substring(0, nextKey.indexOf(".class"));
- if (isBlank(name)) {
- continue;
- }
-
- String className = file.getProperty(nextKey);
-
- Class> clazz;
- try {
- clazz = Class.forName(className);
- } catch (ClassNotFoundException e) {
- ourLog.debug("Unknown datatype class '{}' identified in narrative file {}", name, propFileName);
- clazz = null;
- }
-
- if (clazz != null) {
- myClassToName.put(clazz, name);
- }
-
- } else if (nextKey.endsWith(".narrative")) {
- String name = nextKey.substring(0, nextKey.indexOf(".narrative"));
- if (isBlank(name)) {
- continue;
- }
- String narrativePropName = name + ".narrative";
- String narrativeName = file.getProperty(narrativePropName);
- if (StringUtils.isNotBlank(narrativeName)) {
- String narrative = IOUtils.toString(loadResource(narrativeName), Constants.CHARSET_UTF8);
- myNameToNarrativeTemplate.put(name, narrative);
- }
- continue;
- } else if (nextKey.endsWith(".title")) {
- ourLog.debug("Ignoring title property as narrative generator no longer generates titles: {}", nextKey);
- } else {
- throw new ConfigurationException("Invalid property name: " + nextKey);
- }
-
- }
- }
-
- private InputStream loadResource(String name) throws IOException {
- if (name.startsWith("classpath:")) {
- String cpName = name.substring("classpath:".length());
- InputStream resource = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream(cpName);
- if (resource == null) {
- resource = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream("/" + cpName);
- if (resource == null) {
- throw new IOException("Can not find '" + cpName + "' on classpath");
- }
- }
- //FIXME resource leak
- return resource;
- } else if (name.startsWith("file:")) {
- File file = new File(name.substring("file:".length()));
- if (file.exists() == false) {
- throw new IOException("File not found: " + file.getAbsolutePath());
- }
- return new FileInputStream(file);
- } else {
- throw new IOException("Invalid resource name: '" + name + "' (must start with classpath: or file: )");
- }
- }
-
- /**
- * 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.
- *
- */
- public void setCleanWhitespace(boolean theCleanWhitespace) {
- myCleanWhitespace = theCleanWhitespace;
- }
-
- /**
- * 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
- */
- public void setIgnoreMissingTemplates(boolean theIgnoreMissingTemplates) {
- myIgnoreMissingTemplates = theIgnoreMissingTemplates;
- }
-
- static String cleanWhitespace(String theResult) {
- StringBuilder b = new StringBuilder();
- boolean inWhitespace = false;
- boolean betweenTags = false;
- boolean lastNonWhitespaceCharWasTagEnd = false;
- boolean inPre = false;
- for (int i = 0; i < theResult.length(); i++) {
- char nextChar = theResult.charAt(i);
- if (inPre) {
- b.append(nextChar);
- continue;
- } else if (nextChar == '>') {
- b.append(nextChar);
- betweenTags = true;
- lastNonWhitespaceCharWasTagEnd = true;
- continue;
- } else if (nextChar == '\n' || nextChar == '\r') {
- // if (inWhitespace) {
- // b.append(' ');
- // inWhitespace = false;
- // }
- continue;
- }
-
- if (betweenTags) {
- if (Character.isWhitespace(nextChar)) {
- inWhitespace = true;
- } else if (nextChar == '<') {
- if (inWhitespace && !lastNonWhitespaceCharWasTagEnd) {
- b.append(' ');
- }
- inWhitespace = false;
- b.append(nextChar);
- inWhitespace = false;
- betweenTags = false;
- lastNonWhitespaceCharWasTagEnd = false;
- if (i + 3 < theResult.length()) {
- char char1 = Character.toLowerCase(theResult.charAt(i + 1));
- char char2 = Character.toLowerCase(theResult.charAt(i + 2));
- char char3 = Character.toLowerCase(theResult.charAt(i + 3));
- char char4 = Character.toLowerCase((i + 4 < theResult.length()) ? theResult.charAt(i + 4) : ' ');
- if (char1 == 'p' && char2 == 'r' && char3 == 'e') {
- inPre = true;
- } else if (char1 == '/' && char2 == 'p' && char3 == 'r' && char4 == 'e') {
- inPre = false;
- }
- }
- } else {
- lastNonWhitespaceCharWasTagEnd = false;
- if (inWhitespace) {
- b.append(' ');
- inWhitespace = false;
- }
- b.append(nextChar);
- }
- } else {
- b.append(nextChar);
- }
- }
- return b.toString();
- }
-
- public class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor {
-
- private FhirContext myContext;
-
- protected NarrativeAttributeProcessor(FhirContext theContext, String theDialectPrefix) {
- super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true);
- myContext = theContext;
- }
-
- @SuppressWarnings("unchecked")
- @Override
- protected void doProcess(ITemplateContext theContext, IProcessableElementTag theTag, AttributeName theAttributeName, String theAttributeValue, IElementTagStructureHandler theStructureHandler) {
- IEngineConfiguration configuration = theContext.getConfiguration();
- IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration);
-
- final IStandardExpression expression = expressionParser.parseExpression(theContext, theAttributeValue);
- final Object value = expression.execute(theContext);
-
- if (value == null) {
- return;
- }
-
- Context context = new Context();
- context.setVariable("fhirVersion", myContext.getVersion().getVersion().name());
- context.setVariable("resource", value);
-
- String name = null;
-
- Class extends Object> nextClass = value.getClass();
- do {
- name = myClassToName.get(nextClass);
- nextClass = nextClass.getSuperclass();
- } while (name == null && nextClass.equals(Object.class) == false);
-
- if (name == null) {
- if (value instanceof IBaseResource) {
- name = myContext.getResourceDefinition((Class extends IBaseResource>) value).getName();
- } else if (value instanceof IDatatype) {
- name = value.getClass().getSimpleName();
- name = name.substring(0, name.length() - 2);
- } else if (value instanceof IBaseDatatype) {
- name = value.getClass().getSimpleName();
- if (name.endsWith("Type")) {
- name = name.substring(0, name.length() - 4);
- }
- } else {
- throw new DataFormatException("Don't know how to determine name for type: " + value.getClass());
- }
- name = name.toLowerCase();
- if (!myNameToNarrativeTemplate.containsKey(name)) {
- name = null;
- }
- }
-
- if (name == null) {
- if (myIgnoreMissingTemplates) {
- ourLog.debug("No narrative template available for type: {}", value.getClass());
- return;
- }
- throw new DataFormatException("No narrative template for class " + value.getClass());
- }
-
- String result = myProfileTemplateEngine.process(name, context);
- String trim = result.trim();
-
- theStructureHandler.setBody(trim, true);
-
- }
-
- }
-
- // public class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor {
- //
- // private FhirContext myContext;
- //
- // protected NarrativeAttributeProcessor(FhirContext theContext) {
- // super()
- // myContext = theContext;
- // }
- //
- // @Override
- // public int getPrecedence() {
- // return 0;
- // }
- //
- // @SuppressWarnings("unchecked")
- // @Override
- // protected ProcessorResult processAttribute(Arguments theArguments, Element theElement, String theAttributeName) {
- // final String attributeValue = theElement.getAttributeValue(theAttributeName);
- //
- // final Configuration configuration = theArguments.getConfiguration();
- // final IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration);
- //
- // final IStandardExpression expression = expressionParser.parseExpression(configuration, theArguments, attributeValue);
- // final Object value = expression.execute(configuration, theArguments);
- //
- // theElement.removeAttribute(theAttributeName);
- // theElement.clearChildren();
- //
- // if (value == null) {
- // return ProcessorResult.ok();
- // }
- //
- // Context context = new Context();
- // context.setVariable("fhirVersion", myContext.getVersion().getVersion().name());
- // context.setVariable("resource", value);
- //
- // String name = null;
- // if (value != null) {
- // Class extends Object> nextClass = value.getClass();
- // do {
- // name = myClassToName.get(nextClass);
- // nextClass = nextClass.getSuperclass();
- // } while (name == null && nextClass.equals(Object.class) == false);
- //
- // if (name == null) {
- // if (value instanceof IBaseResource) {
- // name = myContext.getResourceDefinition((Class extends IBaseResource>) value).getName();
- // } else if (value instanceof IDatatype) {
- // name = value.getClass().getSimpleName();
- // name = name.substring(0, name.length() - 2);
- // } else if (value instanceof IBaseDatatype) {
- // name = value.getClass().getSimpleName();
- // if (name.endsWith("Type")) {
- // name = name.substring(0, name.length() - 4);
- // }
- // } else {
- // throw new DataFormatException("Don't know how to determine name for type: " + value.getClass());
- // }
- // name = name.toLowerCase();
- // if (!myNameToNarrativeTemplate.containsKey(name)) {
- // name = null;
- // }
- // }
- // }
- //
- // if (name == null) {
- // if (myIgnoreMissingTemplates) {
- // ourLog.debug("No narrative template available for type: {}", value.getClass());
- // return ProcessorResult.ok();
- // } else {
- // throw new DataFormatException("No narrative template for class " + value.getClass());
- // }
- // }
- //
- // String result = myProfileTemplateEngine.process(name, context);
- // String trim = result.trim();
- // if (!isBlank(trim + "AAA")) {
- // Document dom = getXhtmlDOMFor(new StringReader(trim));
- //
- // Element firstChild = (Element) dom.getFirstChild();
- // for (int i = 0; i < firstChild.getChildren().size(); i++) {
- // Node next = firstChild.getChildren().get(i);
- // if (i == 0 && firstChild.getChildren().size() == 1) {
- // if (next instanceof org.thymeleaf.dom.Text) {
- // org.thymeleaf.dom.Text nextText = (org.thymeleaf.dom.Text) next;
- // nextText.setContent(nextText.getContent().trim());
- // }
- // }
- // theElement.addChild(next);
- // }
- //
- // }
- //
- //
- // return ProcessorResult.ok();
- // }
- //
- // }
-
- // public String generateString(Patient theValue) {
- //
- // Context context = new Context();
- // context.setVariable("resource", theValue);
- // String result =
- // myProfileTemplateEngine.process("ca/uhn/fhir/narrative/Patient.html",
- // context);
- //
- // ourLog.info("Result: {}", result);
- //
- // return result;
- // }
-
- private final class ProfileResourceResolver extends DefaultTemplateResolver {
-
- @Override
- protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) {
- String template = myNameToNarrativeTemplate.get(theTemplate);
- return template != null;
- }
-
- @Override
- protected TemplateMode computeTemplateMode(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) {
- return TemplateMode.XML;
- }
-
- @Override
- protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) {
- String template = myNameToNarrativeTemplate.get(theTemplate);
- return new StringTemplateResource(template);
- }
-
- @Override
- protected ICacheEntryValidity computeValidity(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) {
- return AlwaysValidCacheEntryValidity.INSTANCE;
- }
-
- }
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.java
index e3e75215abe..7e950c5601e 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative/CustomThymeleafNarrativeGenerator.java
@@ -23,6 +23,7 @@ package ca.uhn.fhir.narrative;
import java.util.Arrays;
import java.util.List;
+import ca.uhn.fhir.context.FhirContext;
import org.apache.commons.lang3.Validate;
public class CustomThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGenerator {
@@ -39,7 +40,8 @@ public class CustomThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGen
*
classpath:/com/package/file.properties
*
*/
- public CustomThymeleafNarrativeGenerator(String... thePropertyFile) {
+ public CustomThymeleafNarrativeGenerator(FhirContext theFhirContext, String... thePropertyFile) {
+ super(theFhirContext);
setPropertyFile(thePropertyFile);
}
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 e814e4aa32c..e384bfaff88 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
@@ -20,6 +20,8 @@ package ca.uhn.fhir.narrative;
* #L%
*/
+import ca.uhn.fhir.context.FhirContext;
+
import java.util.ArrayList;
import java.util.List;
@@ -30,6 +32,10 @@ public class DefaultThymeleafNarrativeGenerator extends BaseThymeleafNarrativeGe
private boolean myUseHapiServerConformanceNarrative;
+ public DefaultThymeleafNarrativeGenerator(FhirContext theFhirContext) {
+ super(theFhirContext);
+ }
+
@Override
protected List getPropertyFile() {
List retVal = new ArrayList();
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 5c30287bdc1..35c7dc1397b 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
@@ -21,12 +21,17 @@ package ca.uhn.fhir.narrative;
*/
import org.hl7.fhir.instance.model.api.IBaseResource;
-import org.hl7.fhir.instance.model.api.INarrative;
-
-import ca.uhn.fhir.context.FhirContext;
public interface INarrativeGenerator {
- void generateNarrative(FhirContext theContext, IBaseResource theResource, INarrative theNarrative);
+ /**
+ * Generate any narratives for the given resource that have applicable
+ * templates, and populates the appropriate field(s). This almost always means
+ * the Resource.text.narrative field, but for some resource types
+ * it can mean other fields (e.g. Composition.
+ *
+ * @return Returns true if a narrative was actually generated
+ */
+ boolean populateResourceNarrative(IBaseResource theResource);
}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/BaseNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/BaseNarrativeGenerator.java
new file mode 100644
index 00000000000..38941e3a88b
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/BaseNarrativeGenerator.java
@@ -0,0 +1,221 @@
+package ca.uhn.fhir.narrative2;
+
+/*-
+ * #%L
+ * HAPI FHIR - Core Library
+ * %%
+ * Copyright (C) 2014 - 2019 University Health Network
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
+import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
+import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.context.FhirVersionEnum;
+import ca.uhn.fhir.fluentpath.IFluentPath;
+import ca.uhn.fhir.narrative.INarrativeGenerator;
+import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
+import org.apache.commons.lang3.Validate;
+import org.hl7.fhir.instance.model.api.IBase;
+import org.hl7.fhir.instance.model.api.IBaseResource;
+import org.hl7.fhir.instance.model.api.INarrative;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.apache.commons.lang3.StringUtils.defaultIfEmpty;
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+
+public abstract class BaseNarrativeGenerator implements INarrativeGenerator {
+
+ private INarrativeTemplateManifest myManifest;
+ private final FhirContext myFhirContext;
+
+ public BaseNarrativeGenerator(FhirContext theFhirContext) {
+ Validate.notNull(theFhirContext, "theFhirContext must not be null");
+ myFhirContext = theFhirContext;
+ }
+
+ public INarrativeTemplateManifest getManifest() {
+ return myManifest;
+ }
+
+ public void setManifest(INarrativeTemplateManifest theManifest) {
+ myManifest = theManifest;
+ }
+
+ public FhirContext getFhirContext() {
+ return myFhirContext;
+ }
+
+ @Override
+ public boolean populateResourceNarrative(IBaseResource theResource) {
+ Optional templateOpt = getTemplateForElement(theResource);
+ if (templateOpt.isPresent()) {
+ return applyTemplate(templateOpt.get(), theResource);
+ } else {
+ return false;
+ }
+ }
+
+ private Optional getTemplateForElement(IBase theElement) {
+ return myManifest.getTemplateByElement(getStyle(), theElement);
+ }
+
+ private boolean applyTemplate(INarrativeTemplate theTemplate, IBaseResource theResource) {
+ if (templateDoesntApplyToResource(theTemplate, theResource)) {
+ return false;
+ }
+
+ boolean retVal = false;
+ String resourceName = myFhirContext.getResourceDefinition(theResource).getName();
+ String contextPath = defaultIfEmpty(theTemplate.getContextPath(), resourceName);
+
+ // Narrative templates define a path within the resource that they apply to. Here, we're
+ // finding anywhere in the resource that gets a narrative
+ List targets = findElementsInResourceRequiringNarratives(theResource, contextPath);
+ for (IBase nextTargetContext : targets) {
+
+ // Extract [element].text of type Narrative
+ INarrative nextTargetNarrative = getOrCreateNarrativeChildElement(nextTargetContext);
+
+ // Create the actual narrative text
+ String narrative = applyTemplate(theTemplate, nextTargetContext);
+ narrative = cleanWhitespace(narrative);
+
+ if (isNotBlank(narrative)) {
+ try {
+ nextTargetNarrative.setDivAsString(narrative);
+ nextTargetNarrative.setStatusAsString("generated");
+ retVal = true;
+ } catch (Exception e) {
+ throw new InternalErrorException(e);
+ }
+ }
+
+ }
+ return retVal;
+ }
+
+ private INarrative getOrCreateNarrativeChildElement(IBase nextTargetContext) {
+ BaseRuntimeElementCompositeDefinition> targetElementDef = (BaseRuntimeElementCompositeDefinition>) getFhirContext().getElementDefinition(nextTargetContext.getClass());
+ BaseRuntimeChildDefinition targetTextChild = targetElementDef.getChildByName("text");
+ List existing = targetTextChild.getAccessor().getValues(nextTargetContext);
+ INarrative nextTargetNarrative;
+ if (existing.isEmpty()) {
+ nextTargetNarrative = (INarrative) getFhirContext().getElementDefinition("narrative").newInstance();
+ targetTextChild.getMutator().addValue(nextTargetContext, nextTargetNarrative);
+ } else {
+ nextTargetNarrative = (INarrative) existing.get(0);
+ }
+ return nextTargetNarrative;
+ }
+
+ private List findElementsInResourceRequiringNarratives(IBaseResource theResource, String theContextPath) {
+ if (myFhirContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
+ return Collections.singletonList(theResource);
+ }
+ IFluentPath fhirPath = myFhirContext.newFluentPath();
+ return fhirPath.evaluate(theResource, theContextPath, IBase.class);
+ }
+
+ protected abstract String applyTemplate(INarrativeTemplate theTemplate, IBase theTargetContext);
+
+ private boolean templateDoesntApplyToResource(INarrativeTemplate theTemplate, IBaseResource theResource) {
+ boolean retVal = false;
+ if (theTemplate.getAppliesToProfiles() != null && !theTemplate.getAppliesToProfiles().isEmpty()) {
+ Set resourceProfiles = theResource
+ .getMeta()
+ .getProfile()
+ .stream()
+ .map(t -> t.getValueAsString())
+ .collect(Collectors.toSet());
+ retVal = true;
+ for (String next : theTemplate.getAppliesToProfiles()) {
+ if (resourceProfiles.contains(next)) {
+ retVal = false;
+ break;
+ }
+ }
+ }
+ return retVal;
+ }
+
+ protected abstract TemplateTypeEnum getStyle();
+
+ /**
+ * Trims the superfluous whitespace out of an HTML block
+ */
+ public static String cleanWhitespace(String theResult) {
+ StringBuilder b = new StringBuilder();
+ boolean inWhitespace = false;
+ boolean betweenTags = false;
+ boolean lastNonWhitespaceCharWasTagEnd = false;
+ boolean inPre = false;
+ for (int i = 0; i < theResult.length(); i++) {
+ char nextChar = theResult.charAt(i);
+ if (inPre) {
+ b.append(nextChar);
+ continue;
+ } else if (nextChar == '>') {
+ b.append(nextChar);
+ betweenTags = true;
+ lastNonWhitespaceCharWasTagEnd = true;
+ continue;
+ } else if (nextChar == '\n' || nextChar == '\r') {
+ continue;
+ }
+
+ if (betweenTags) {
+ if (Character.isWhitespace(nextChar)) {
+ inWhitespace = true;
+ } else if (nextChar == '<') {
+ if (inWhitespace && !lastNonWhitespaceCharWasTagEnd) {
+ b.append(' ');
+ }
+ inWhitespace = false;
+ b.append(nextChar);
+ inWhitespace = false;
+ betweenTags = false;
+ lastNonWhitespaceCharWasTagEnd = false;
+ if (i + 3 < theResult.length()) {
+ char char1 = Character.toLowerCase(theResult.charAt(i + 1));
+ char char2 = Character.toLowerCase(theResult.charAt(i + 2));
+ char char3 = Character.toLowerCase(theResult.charAt(i + 3));
+ char char4 = Character.toLowerCase((i + 4 < theResult.length()) ? theResult.charAt(i + 4) : ' ');
+ if (char1 == 'p' && char2 == 'r' && char3 == 'e') {
+ inPre = true;
+ } else if (char1 == '/' && char2 == 'p' && char3 == 'r' && char4 == 'e') {
+ inPre = false;
+ }
+ }
+ } else {
+ lastNonWhitespaceCharWasTagEnd = false;
+ if (inWhitespace) {
+ b.append(' ');
+ inWhitespace = false;
+ }
+ b.append(nextChar);
+ }
+ } else {
+ b.append(nextChar);
+ }
+ }
+ return b.toString();
+ }
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplate.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplate.java
new file mode 100644
index 00000000000..0f37c85435b
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplate.java
@@ -0,0 +1,42 @@
+package ca.uhn.fhir.narrative2;
+
+/*-
+ * #%L
+ * HAPI FHIR - Core Library
+ * %%
+ * Copyright (C) 2014 - 2019 University Health Network
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import org.hl7.fhir.instance.model.api.IBase;
+
+import java.io.IOException;
+import java.util.Set;
+
+public interface INarrativeTemplate {
+ String getContextPath();
+
+ Set getAppliesToProfiles();
+
+ Set getAppliesToResourceTypes();
+
+ Set> getAppliesToResourceClasses();
+
+ TemplateTypeEnum getTemplateType();
+
+ String getTemplateName();
+
+ String getTemplateText();
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java
new file mode 100644
index 00000000000..15b28d8cd2e
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/INarrativeTemplateManifest.java
@@ -0,0 +1,33 @@
+package ca.uhn.fhir.narrative2;
+
+/*-
+ * #%L
+ * HAPI FHIR - Core Library
+ * %%
+ * Copyright (C) 2014 - 2019 University Health Network
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import org.hl7.fhir.instance.model.api.IBase;
+
+import java.util.Optional;
+
+public interface INarrativeTemplateManifest {
+ Optional getTemplateByResourceName(TemplateTypeEnum theStyle, String theResourceName);
+
+ Optional getTemplateByName(TemplateTypeEnum theStyle, String theName);
+
+ Optional getTemplateByElement(TemplateTypeEnum theStyle, IBase theElementValue);
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java
new file mode 100644
index 00000000000..3d16becb484
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplate.java
@@ -0,0 +1,121 @@
+package ca.uhn.fhir.narrative2;
+
+/*-
+ * #%L
+ * HAPI FHIR - Core Library
+ * %%
+ * Copyright (C) 2014 - 2019 University Health Network
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
+import org.hl7.fhir.instance.model.api.IBase;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class NarrativeTemplate implements INarrativeTemplate {
+
+ private String myTemplateFileName;
+ private Set myAppliesToProfiles = new HashSet<>();
+ private Set myAppliesToResourceTypes = new HashSet<>();
+ private Set myAppliesToDataTypes = new HashSet<>();
+ private Set> myAppliesToResourceClasses = new HashSet<>();
+ private TemplateTypeEnum myTemplateType = TemplateTypeEnum.THYMELEAF;
+ private String myContextPath;
+ private String myTemplateName;
+
+ public Set getAppliesToDataTypes() {
+ return Collections.unmodifiableSet(myAppliesToDataTypes);
+ }
+
+ @Override
+ public String getContextPath() {
+ return myContextPath;
+ }
+
+ public void setContextPath(String theContextPath) {
+ myContextPath = theContextPath;
+ }
+
+ private String getTemplateFileName() {
+ return myTemplateFileName;
+ }
+
+ void setTemplateFileName(String theTemplateFileName) {
+ myTemplateFileName = theTemplateFileName;
+ }
+
+ @Override
+ public Set getAppliesToProfiles() {
+ return Collections.unmodifiableSet(myAppliesToProfiles);
+ }
+
+ void addAppliesToProfile(String theAppliesToProfile) {
+ myAppliesToProfiles.add(theAppliesToProfile);
+ }
+
+ @Override
+ public Set getAppliesToResourceTypes() {
+ return Collections.unmodifiableSet(myAppliesToResourceTypes);
+ }
+
+ void addAppliesToResourceType(String theAppliesToResourceType) {
+ myAppliesToResourceTypes.add(theAppliesToResourceType);
+ }
+
+ @Override
+ public Set> getAppliesToResourceClasses() {
+ return Collections.unmodifiableSet(myAppliesToResourceClasses);
+ }
+
+ void addAppliesToResourceClass(Class extends IBase> theAppliesToResourceClass) {
+ myAppliesToResourceClasses.add(theAppliesToResourceClass);
+ }
+
+ @Override
+ public TemplateTypeEnum getTemplateType() {
+ return myTemplateType;
+ }
+
+ void setTemplateType(TemplateTypeEnum theTemplateType) {
+ myTemplateType = theTemplateType;
+ }
+
+ @Override
+ public String getTemplateName() {
+ return myTemplateName;
+ }
+
+ NarrativeTemplate setTemplateName(String theTemplateName) {
+ myTemplateName = theTemplateName;
+ return this;
+ }
+
+ @Override
+ public String getTemplateText() {
+ try {
+ return NarrativeTemplateManifest.loadResource(getTemplateFileName());
+ } catch (IOException e) {
+ throw new InternalErrorException(e);
+ }
+ }
+
+ void addAppliesToDatatype(String theDataType) {
+ myAppliesToDataTypes.add(theDataType);
+ }
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java
new file mode 100644
index 00000000000..3e5a7a4ae51
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NarrativeTemplateManifest.java
@@ -0,0 +1,239 @@
+package ca.uhn.fhir.narrative2;
+
+/*-
+ * #%L
+ * HAPI FHIR - Core Library
+ * %%
+ * Copyright (C) 2014 - 2019 University Health Network
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import ca.uhn.fhir.context.ConfigurationException;
+import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator;
+import com.google.common.base.Charsets;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.hl7.fhir.instance.model.api.IBase;
+import org.hl7.fhir.instance.model.api.IBaseResource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.util.*;
+
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+
+public class NarrativeTemplateManifest implements INarrativeTemplateManifest {
+ private static final Logger ourLog = LoggerFactory.getLogger(NarrativeTemplateManifest.class);
+
+ private final Map> myStyleToResourceTypeToTemplate;
+ private final Map> myStyleToDatatypeToTemplate;
+ private final Map> myStyleToNameToTemplate;
+ private final FhirContext myCtx;
+ private final int myTemplateCount;
+
+ private NarrativeTemplateManifest(FhirContext theFhirContext, Collection theTemplates) {
+ myCtx = theFhirContext;
+ Map> styleToResourceTypeToTemplate = new HashMap<>();
+ Map> styleToDatatypeToTemplate = new HashMap<>();
+ Map> styleToNameToTemplate = new HashMap<>();
+
+ for (NarrativeTemplate nextTemplate : theTemplates) {
+ Map resourceTypeToTemplate = styleToResourceTypeToTemplate.computeIfAbsent(nextTemplate.getTemplateType(), t -> new HashMap<>());
+ Map datatypeToTemplate = styleToDatatypeToTemplate.computeIfAbsent(nextTemplate.getTemplateType(), t -> new HashMap<>());
+ Map nameToTemplate = styleToNameToTemplate.computeIfAbsent(nextTemplate.getTemplateType(), t -> new HashMap<>());
+ nameToTemplate.put(nextTemplate.getTemplateName(), nextTemplate);
+ for (String nextResourceType : nextTemplate.getAppliesToResourceTypes()) {
+ resourceTypeToTemplate.put(nextResourceType.toUpperCase(), nextTemplate);
+ }
+ for (String nextDataType : nextTemplate.getAppliesToDataTypes()) {
+ datatypeToTemplate.put(nextDataType.toUpperCase(), nextTemplate);
+ }
+ }
+
+ myTemplateCount = theTemplates.size();
+ myStyleToNameToTemplate = makeImmutable(styleToNameToTemplate);
+ myStyleToResourceTypeToTemplate = makeImmutable(styleToResourceTypeToTemplate);
+ myStyleToDatatypeToTemplate = makeImmutable(styleToDatatypeToTemplate);
+ }
+
+ public int getNamedTemplateCount() {
+ return myTemplateCount;
+ }
+
+ @Override
+ public Optional getTemplateByResourceName(TemplateTypeEnum theStyle, String theResourceName) {
+ return getFromMap(theStyle, theResourceName.toUpperCase(), myStyleToResourceTypeToTemplate);
+ }
+
+ @Override
+ public Optional getTemplateByName(TemplateTypeEnum theStyle, String theName) {
+ return getFromMap(theStyle, theName, myStyleToNameToTemplate);
+ }
+
+ @Override
+ public Optional getTemplateByElement(TemplateTypeEnum theStyle, IBase theElement) {
+ if (theElement instanceof IBaseResource) {
+ String resourceName = myCtx.getResourceDefinition((IBaseResource) theElement).getName();
+ return getTemplateByResourceName(theStyle, resourceName);
+ } else {
+ String datatypeName = myCtx.getElementDefinition(theElement.getClass()).getName();
+ return getFromMap(theStyle, datatypeName.toUpperCase(), myStyleToDatatypeToTemplate);
+ }
+ }
+
+ public static NarrativeTemplateManifest forManifestFileLocation(FhirContext theFhirContext, String... thePropertyFilePaths) throws IOException {
+ return forManifestFileLocation(theFhirContext, Arrays.asList(thePropertyFilePaths));
+ }
+
+ public static NarrativeTemplateManifest forManifestFileLocation(FhirContext theFhirContext, Collection thePropertyFilePaths) throws IOException {
+ ourLog.debug("Loading narrative properties file(s): {}", thePropertyFilePaths);
+
+ List manifestFileContents = new ArrayList<>(thePropertyFilePaths.size());
+ for (String next : thePropertyFilePaths) {
+ String resource = loadResource(next);
+ manifestFileContents.add(resource);
+ }
+
+ return forManifestFileContents(theFhirContext, manifestFileContents);
+ }
+
+ public static NarrativeTemplateManifest forManifestFileContents(FhirContext theFhirContext, String... theResources) throws IOException {
+ return forManifestFileContents(theFhirContext, Arrays.asList(theResources));
+ }
+
+ public static NarrativeTemplateManifest forManifestFileContents(FhirContext theFhirContext, Collection theResources) throws IOException {
+ List templates = new ArrayList<>();
+ for (String next : theResources) {
+ templates.addAll(loadProperties(next));
+ }
+ return new NarrativeTemplateManifest(theFhirContext, templates);
+ }
+
+ private static Collection loadProperties(String theManifestText) throws IOException {
+ Map nameToTemplate = new HashMap<>();
+
+ Properties file = new Properties();
+
+ file.load(new StringReader(theManifestText));
+ for (Object nextKeyObj : file.keySet()) {
+ String nextKey = (String) nextKeyObj;
+ Validate.isTrue(StringUtils.countMatches(nextKey, ".") == 1, "Invalid narrative property file key: %s", nextKey);
+ String name = nextKey.substring(0, nextKey.indexOf('.'));
+ Validate.notBlank(name, "Invalid narrative property file key: %s", nextKey);
+
+ NarrativeTemplate nextTemplate = nameToTemplate.computeIfAbsent(name, t -> new NarrativeTemplate().setTemplateName(name));
+
+ Validate.isTrue(!nextKey.endsWith(".class"), "Narrative manifest does not support specifying templates by class name - Use \"[name].resourceType=[resourceType]\" instead");
+
+ if (nextKey.endsWith(".profile")) {
+ String profile = file.getProperty(nextKey);
+ if (isNotBlank(profile)) {
+ nextTemplate.addAppliesToProfile(profile);
+ }
+ } else if (nextKey.endsWith(".resourceType")) {
+ String resourceType = file.getProperty(nextKey);
+ Arrays
+ .stream(resourceType.split(","))
+ .map(t -> t.trim())
+ .filter(t -> isNotBlank(t))
+ .forEach(t -> nextTemplate.addAppliesToResourceType(t));
+ } else if (nextKey.endsWith(".dataType")) {
+ String dataType = file.getProperty(nextKey);
+ Arrays
+ .stream(dataType.split(","))
+ .map(t -> t.trim())
+ .filter(t -> isNotBlank(t))
+ .forEach(t -> nextTemplate.addAppliesToDatatype(t));
+ } else if (nextKey.endsWith(".class")) {
+ String className = file.getProperty(nextKey);
+ Class extends IBase> clazz;
+ try {
+ clazz = (Class extends IBase>) Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ ourLog.debug("Unknown datatype class '{}' identified in manifest", name);
+ clazz = null;
+ }
+ if (clazz != null) {
+ nextTemplate.addAppliesToResourceClass(clazz);
+ }
+ } else if (nextKey.endsWith(".style")) {
+ String templateTypeName = file.getProperty(nextKey).toUpperCase();
+ TemplateTypeEnum templateType = TemplateTypeEnum.valueOf(templateTypeName);
+ nextTemplate.setTemplateType(templateType);
+ } else if (nextKey.endsWith(".contextPath")) {
+ String contextPath = file.getProperty(nextKey);
+ nextTemplate.setContextPath(contextPath);
+ } else if (nextKey.endsWith(".narrative")) {
+ String narrativePropName = name + ".narrative";
+ String narrativeName = file.getProperty(narrativePropName);
+ if (StringUtils.isNotBlank(narrativeName)) {
+ nextTemplate.setTemplateFileName(narrativeName);
+ }
+ } else if (nextKey.endsWith(".title")) {
+ ourLog.debug("Ignoring title property as narrative generator no longer generates titles: {}", nextKey);
+ } else {
+ throw new ConfigurationException("Invalid property name: " + nextKey);
+ }
+
+ }
+
+ return nameToTemplate.values();
+ }
+
+ static String loadResource(String name) throws IOException {
+ if (name.startsWith("classpath:")) {
+ String cpName = name.substring("classpath:".length());
+ try (InputStream resource = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream(cpName)) {
+ if (resource == null) {
+ try (InputStream resource2 = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream("/" + cpName)) {
+ if (resource2 == null) {
+ throw new IOException("Can not find '" + cpName + "' on classpath");
+ }
+ return IOUtils.toString(resource2, Charsets.UTF_8);
+ }
+ }
+ return IOUtils.toString(resource, Charsets.UTF_8);
+ }
+ } else if (name.startsWith("file:")) {
+ File file = new File(name.substring("file:".length()));
+ if (file.exists() == false) {
+ throw new IOException("File not found: " + file.getAbsolutePath());
+ }
+ try (FileInputStream inputStream = new FileInputStream(file)) {
+ return IOUtils.toString(inputStream, Charsets.UTF_8);
+ }
+ } else {
+ throw new IOException("Invalid resource name: '" + name + "' (must start with classpath: or file: )");
+ }
+ }
+
+ private static Optional getFromMap(TemplateTypeEnum theStyle, T theResourceName, Map> theMap) {
+ NarrativeTemplate retVal = null;
+ Map resourceTypeToTemplate = theMap.get(theStyle);
+ if (resourceTypeToTemplate != null) {
+ retVal = resourceTypeToTemplate.get(theResourceName);
+ }
+ return Optional.ofNullable(retVal);
+ }
+
+ private static Map> makeImmutable(Map> theStyleToResourceTypeToTemplate) {
+ theStyleToResourceTypeToTemplate.replaceAll((key, value) -> Collections.unmodifiableMap(value));
+ return Collections.unmodifiableMap(theStyleToResourceTypeToTemplate);
+ }
+
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NullNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NullNarrativeGenerator.java
new file mode 100644
index 00000000000..ed3f3ef1c72
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/NullNarrativeGenerator.java
@@ -0,0 +1,31 @@
+package ca.uhn.fhir.narrative2;
+
+/*-
+ * #%L
+ * HAPI FHIR - Core Library
+ * %%
+ * Copyright (C) 2014 - 2019 University Health Network
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import ca.uhn.fhir.narrative.INarrativeGenerator;
+import org.hl7.fhir.instance.model.api.IBaseResource;
+
+public class NullNarrativeGenerator implements INarrativeGenerator {
+ @Override
+ public boolean populateResourceNarrative(IBaseResource theResource) {
+ return false;
+ }
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/TemplateTypeEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/TemplateTypeEnum.java
new file mode 100644
index 00000000000..89d4305cf84
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/TemplateTypeEnum.java
@@ -0,0 +1,28 @@
+package ca.uhn.fhir.narrative2;
+
+/*-
+ * #%L
+ * HAPI FHIR - Core Library
+ * %%
+ * Copyright (C) 2014 - 2019 University Health Network
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+public enum TemplateTypeEnum {
+
+ THYMELEAF,
+ LIQUID
+
+}
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGenerator.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGenerator.java
new file mode 100644
index 00000000000..bfffff0f85e
--- /dev/null
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/narrative2/ThymeleafNarrativeGenerator.java
@@ -0,0 +1,195 @@
+package ca.uhn.fhir.narrative2;
+
+/*-
+ * #%L
+ * HAPI FHIR - Core Library
+ * %%
+ * Copyright (C) 2014 - 2019 University Health Network
+ * %%
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * #L%
+ */
+
+import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
+import org.hl7.fhir.instance.model.api.IBase;
+import org.thymeleaf.IEngineConfiguration;
+import org.thymeleaf.TemplateEngine;
+import org.thymeleaf.cache.AlwaysValidCacheEntryValidity;
+import org.thymeleaf.cache.ICacheEntryValidity;
+import org.thymeleaf.context.Context;
+import org.thymeleaf.context.ITemplateContext;
+import org.thymeleaf.engine.AttributeName;
+import org.thymeleaf.messageresolver.IMessageResolver;
+import org.thymeleaf.model.IProcessableElementTag;
+import org.thymeleaf.processor.IProcessor;
+import org.thymeleaf.processor.element.AbstractAttributeTagProcessor;
+import org.thymeleaf.processor.element.AbstractElementTagProcessor;
+import org.thymeleaf.processor.element.IElementTagStructureHandler;
+import org.thymeleaf.standard.StandardDialect;
+import org.thymeleaf.standard.expression.IStandardExpression;
+import org.thymeleaf.standard.expression.IStandardExpressionParser;
+import org.thymeleaf.standard.expression.StandardExpressions;
+import org.thymeleaf.templatemode.TemplateMode;
+import org.thymeleaf.templateresolver.DefaultTemplateResolver;
+import org.thymeleaf.templateresource.ITemplateResource;
+import org.thymeleaf.templateresource.StringTemplateResource;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+
+public class ThymeleafNarrativeGenerator extends BaseNarrativeGenerator {
+
+ private IMessageResolver myMessageResolver;
+
+ /**
+ * Constructor
+ */
+ public ThymeleafNarrativeGenerator(FhirContext theFhirContext) {
+ super(theFhirContext);
+ }
+
+ private TemplateEngine getTemplateEngine() {
+ TemplateEngine engine = new TemplateEngine();
+ ProfileResourceResolver resolver = new ProfileResourceResolver();
+ engine.setTemplateResolver(resolver);
+ if (myMessageResolver != null) {
+ engine.setMessageResolver(myMessageResolver);
+ }
+ StandardDialect dialect = new StandardDialect() {
+ @Override
+ public Set getProcessors(String theDialectPrefix) {
+ Set retVal = super.getProcessors(theDialectPrefix);
+ retVal.add(new NarrativeTagProcessor(theDialectPrefix));
+ retVal.add(new NarrativeAttributeProcessor(theDialectPrefix));
+ return retVal;
+ }
+
+ };
+
+ engine.setDialect(dialect);
+ return engine;
+ }
+
+ @Override
+ protected String applyTemplate(INarrativeTemplate theTemplate, IBase theTargetContext) {
+
+ Context context = new Context();
+ context.setVariable("resource", theTargetContext);
+ context.setVariable("context", theTargetContext);
+ context.setVariable("fhirVersion", getFhirContext().getVersion().getVersion().name());
+
+ String result = getTemplateEngine().process(theTemplate.getTemplateName(), context);
+
+ return result;
+ }
+
+
+ @Override
+ protected TemplateTypeEnum getStyle() {
+ return TemplateTypeEnum.THYMELEAF;
+ }
+
+ private String applyTemplateWithinTag(ITemplateContext theTemplateContext, String theName, String theElement) {
+ IEngineConfiguration configuration = theTemplateContext.getConfiguration();
+ IStandardExpressionParser expressionParser = StandardExpressions.getExpressionParser(configuration);
+ final IStandardExpression expression = expressionParser.parseExpression(theTemplateContext, theElement);
+ Object elementValueObj = expression.execute(theTemplateContext);
+ final IBase elementValue = (IBase) elementValueObj;
+ if (elementValue == null) {
+ return "";
+ }
+
+ Optional templateOpt;
+ if (isNotBlank(theName)) {
+ templateOpt = getManifest().getTemplateByName(getStyle(), theName);
+ if (!templateOpt.isPresent()) {
+ throw new InternalErrorException("Unknown template name: " + theName);
+ }
+ } else {
+ templateOpt = getManifest().getTemplateByElement(getStyle(), elementValue);
+ if (!templateOpt.isPresent()) {
+ throw new InternalErrorException("No template for type: " + elementValue.getClass());
+ }
+ }
+
+ return applyTemplate(templateOpt.get(), elementValue);
+ }
+
+ public void setMessageResolver(IMessageResolver theMessageResolver) {
+ myMessageResolver = theMessageResolver;
+ }
+
+
+ private class ProfileResourceResolver extends DefaultTemplateResolver {
+ @Override
+ protected boolean computeResolvable(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) {
+ return getManifest().getTemplateByName(getStyle(), theTemplate).isPresent();
+ }
+
+ @Override
+ protected TemplateMode computeTemplateMode(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) {
+ return TemplateMode.XML;
+ }
+
+ @Override
+ protected ITemplateResource computeTemplateResource(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) {
+ return getManifest()
+ .getTemplateByName(getStyle(), theTemplate)
+ .map(t -> new StringTemplateResource(t.getTemplateText()))
+ .orElseThrow(() -> new IllegalArgumentException("Unknown template: " + theTemplate));
+ }
+
+ @Override
+ protected ICacheEntryValidity computeValidity(IEngineConfiguration theConfiguration, String theOwnerTemplate, String theTemplate, Map theTemplateResolutionAttributes) {
+ return AlwaysValidCacheEntryValidity.INSTANCE;
+ }
+ }
+
+ private class NarrativeTagProcessor extends AbstractElementTagProcessor {
+
+ public NarrativeTagProcessor(String dialectPrefix) {
+ super(TemplateMode.XML, dialectPrefix, "narrative", true, null, true, 0);
+ }
+
+ @Override
+ protected void doProcess(ITemplateContext theTemplateContext, IProcessableElementTag theTag, IElementTagStructureHandler theStructureHandler) {
+ String name = theTag.getAttributeValue("th:name");
+ String element = theTag.getAttributeValue("th:element");
+
+ String appliedTemplate = applyTemplateWithinTag(theTemplateContext, name, element);
+ theStructureHandler.replaceWith(appliedTemplate, false);
+ }
+ }
+
+ /**
+ * This is a thymeleaf extension that allows people to do things like
+ *
+ */
+ private class NarrativeAttributeProcessor extends AbstractAttributeTagProcessor {
+
+ protected NarrativeAttributeProcessor(String theDialectPrefix) {
+ super(TemplateMode.XML, theDialectPrefix, null, false, "narrative", true, 0, true);
+ }
+
+ @Override
+ protected void doProcess(ITemplateContext theContext, IProcessableElementTag theTag, AttributeName theAttributeName, String theAttributeValue, IElementTagStructureHandler theStructureHandler) {
+ String text = applyTemplateWithinTag(theContext, null, theAttributeValue);
+ theStructureHandler.setBody(text, false);
+ }
+
+ }
+}
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 9ff73b238ae..596e2105701 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
@@ -360,7 +360,7 @@ public class JsonParser extends BaseParser implements IJsonLikeParser {
narr = null;
}
if (narr != null && narr.isEmpty()) {
- gen.generateNarrative(myContext, theResource, narr);
+ gen.populateResourceNarrative(theResource);
if (!narr.isEmpty()) {
RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild;
String childName = nextChild.getChildNameByDatatype(child.getDatatype());
diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java
index 88327cecede..5995bcd490d 100644
--- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java
+++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/XmlParser.java
@@ -371,7 +371,7 @@ public class XmlParser extends BaseParser /* implements IParser */ {
}
// FIXME potential null access on narr see line 623
if (gen != null && narr.isEmpty()) {
- gen.generateNarrative(myContext, theResource, narr);
+ gen.populateResourceNarrative(theResource);
}
if (narr != null && narr.isEmpty() == false) {
RuntimeChildNarrativeDefinition child = (RuntimeChildNarrativeDefinition) nextChild;
diff --git a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBaseReference.java b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBaseReference.java
index a62ef55e22f..1d8f4ecc7ad 100644
--- a/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBaseReference.java
+++ b/hapi-fhir-base/src/main/java/org/hl7/fhir/instance/model/api/IBaseReference.java
@@ -25,7 +25,7 @@ public interface IBaseReference extends ICompositeType {
IBaseResource getResource();
- void setResource(IBaseResource theResource);
+ IBaseReference setResource(IBaseResource theResource);
IIdType getReferenceElement();
diff --git a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/narratives-hapiserver.properties b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/narratives-hapiserver.properties
index 8569d7ebdcc..d3822d3bd6e 100644
--- a/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/narratives-hapiserver.properties
+++ b/hapi-fhir-base/src/main/resources/ca/uhn/fhir/narrative/narratives-hapiserver.properties
@@ -3,6 +3,6 @@
# Resources
################################################
-conformance.class=ca.uhn.fhir.model.dstu.resource.Conformance
-conformance.narrative=classpath:ca/uhn/fhir/narrative/ConformanceHapiServer.html
+conformance.resourceType =Conformance
+conformance.narrative =classpath:ca/uhn/fhir/narrative/ConformanceHapiServer.html
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 58e3c0464b7..c74fc54fa32 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
@@ -3,66 +3,62 @@
# Primitive Datatypes
################################################
-code.class=ca.uhn.fhir.model.primitive.CodeDt
+code.dataType=code
code.narrative=classpath:ca/uhn/fhir/narrative/datatype/CodeDt.html
-datetime.class=ca.uhn.fhir.model.primitive.DateTimeDt
+datetime.dataType=dateTime, instant
datetime.narrative=classpath:ca/uhn/fhir/narrative/datatype/DateTimeDt.html
-# Instant uses DateTime narrative
-instant.class=ca.uhn.fhir.model.primitive.InstantDt
-instant.narrative=classpath:ca/uhn/fhir/narrative/datatype/DateTimeDt.html
-
-string.class=ca.uhn.fhir.model.primitive.StringDt
+string.dataType=string
string.narrative=classpath:ca/uhn/fhir/narrative/datatype/StringDt.html
################################################
# Composite Datatypes
################################################
-address.class=ca.uhn.fhir.model.dstu.composite.AddressDt
+address.dataType=Address
address.narrative=classpath:ca/uhn/fhir/narrative/datatype/AddressDt.html
-codeableconcept.class=ca.uhn.fhir.model.dstu.composite.CodeableConceptDt
+codeableconcept.dataType=CodeableConcept
codeableconcept.narrative=classpath:ca/uhn/fhir/narrative/datatype/CodeableConceptDt.html
boundcodeableconcept.narrative=classpath:ca/uhn/fhir/narrative/datatype/CodeableConceptDt.html
-humanname.class=ca.uhn.fhir.model.dstu.composite.HumanNameDt
+humanname.dataType=HumanName
humanname.narrative=classpath:ca/uhn/fhir/narrative/datatype/HumanNameDt.html
-identifier.class=ca.uhn.fhir.model.dstu.composite.IdentifierDt
+identifier.dataType=Identifier
identifier.narrative=classpath:ca/uhn/fhir/narrative/datatype/IdentifierDt.html
-period.class=ca.uhn.fhir.model.dstu.composite.PeriodDt
+period.dataType=Period
period.narrative=classpath:ca/uhn/fhir/narrative/datatype/PeriodDt.html
-quantity.class=ca.uhn.fhir.model.dstu.composite.QuantityDt
+quantity.dataType=Quantity
quantity.narrative=classpath:ca/uhn/fhir/narrative/datatype/QuantityDt.html
-simplequantity.class=ca.uhn.fhir.model.dstu.composite.SimpleQuantityDt
+simplequantity.dataType=SimpleQuantity
simplequantity.narrative=classpath:ca/uhn/fhir/narrative/datatype/SimpleQuantityDt.html
################################################
# Resources
################################################
-diagnosticreport.class=ca.uhn.fhir.model.dstu.resource.DiagnosticReport
+diagnosticreport.resourceType=DiagnosticReport
diagnosticreport.narrative=classpath:ca/uhn/fhir/narrative/DiagnosticReport.html
-encounter.class=ca.uhn.fhir.model.dstu.resource.Encounter
+#encounter.class=ca.uhn.fhir.model.dstu.resource.Encounter
-operationoutcome.class=ca.uhn.fhir.model.dstu.resource.OperationOutcome
+operationoutcome.resourceType=OperationOutcome
operationoutcome.narrative=classpath:ca/uhn/fhir/narrative/OperationOutcome.html
-organization.class=ca.uhn.fhir.model.dstu.resource.Organization
+#organization.class=ca.uhn.fhir.model.dstu.resource.Organization
-patient.class=ca.uhn.fhir.model.dstu.resource.Patient
+patient.resourceType=Patient
patient.narrative=classpath:ca/uhn/fhir/narrative/Patient.html
-medicationprescription.class=ca.uhn.fhir.model.dstu.resource.MedicationPrescription
+medicationprescription.resourceType=MedicationPrescription
medicationprescription.narrative=classpath:ca/uhn/fhir/narrative/MedicationPrescription.html
-medication.class=ca.uhn.fhir.model.dstu.resource.Medication
+medication.resourceType=Medication
medication.narrative=classpath:ca/uhn/fhir/narrative/Medication.html
medication.title=classpath:ca/uhn/fhir/narrative/Medication.html
diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ExampleDataUploader.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ExampleDataUploader.java
index 01a60cd2cfb..e928cc0e425 100644
--- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ExampleDataUploader.java
+++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ExampleDataUploader.java
@@ -44,7 +44,7 @@ import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.io.FileUtils;
-import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
+import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
import org.hl7.fhir.dstu3.model.Bundle.BundleType;
diff --git a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ValidateCommand.java b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ValidateCommand.java
index ff4dcbb26d2..f210de305d2 100644
--- a/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ValidateCommand.java
+++ b/hapi-fhir-cli/hapi-fhir-cli-api/src/main/java/ca/uhn/fhir/cli/ValidateCommand.java
@@ -35,7 +35,7 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.text.WordUtils;
import org.fusesource.jansi.Ansi.Color;
import org.hl7.fhir.dstu3.hapi.ctx.IValidationSupport;
-import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
+import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
import org.hl7.fhir.dstu3.hapi.validation.FhirInstanceValidator;
import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain;
import org.hl7.fhir.dstu3.model.StructureDefinition;
diff --git a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java
index c2bff0b1cfd..ada28037c90 100644
--- a/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java
+++ b/hapi-fhir-cli/hapi-fhir-cli-jpaserver/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java
@@ -125,7 +125,7 @@ public class JpaServerDemo extends RestfulServer {
* This server tries to dynamically generate narratives
*/
FhirContext ctx = getFhirContext();
- ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
+ ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
/*
* Default to XML and pretty printing
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
index 37646e2add4..3b481f7f7f8 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java
@@ -91,6 +91,8 @@ public abstract class BaseHapiFhirResourceDao extends B
private String myResourceName;
private Class myResourceType;
private String mySecondaryPrimaryKeyParamName;
+ @Autowired
+ private IResourceReindexingSvc myResourceReindexingSvc;
@Override
public void addTag(IIdType theId, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
@@ -572,10 +574,10 @@ public abstract class BaseHapiFhirResourceDao extends B
TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
- BaseHasResource entity = txTemplate.execute(t->readEntity(theId));
+ BaseHasResource entity = txTemplate.execute(t -> readEntity(theId));
if (theId.hasVersionIdPart()) {
BaseHasResource currentVersion;
- currentVersion = txTemplate.execute(t->readEntity(theId.toVersionless()));
+ currentVersion = txTemplate.execute(t -> readEntity(theId.toVersionless()));
if (entity.getVersion() == currentVersion.getVersion()) {
throw new PreconditionFailedException("Can not perform version-specific expunge of resource " + theId.toUnqualified().getValue() + " as this is the current version");
}
@@ -682,7 +684,7 @@ public abstract class BaseHapiFhirResourceDao extends B
TransactionTemplate txTemplate = new TransactionTemplate(myPlatformTransactionManager);
txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
- txTemplate.execute(t->{
+ txTemplate.execute(t -> {
myResourceReindexingSvc.markAllResourcesForReindexing(resourceType);
return null;
});
@@ -694,9 +696,6 @@ public abstract class BaseHapiFhirResourceDao extends B
mySearchParamRegistry.requestRefresh();
}
- @Autowired
- private IResourceReindexingSvc myResourceReindexingSvc;
-
@Override
public MT metaAddOperation(IIdType theResourceId, MT theMetaAdd, RequestDetails theRequestDetails) {
// Notify interceptors
@@ -722,7 +721,7 @@ public abstract class BaseHapiFhirResourceDao extends B
doMetaAdd(theMetaAdd, history);
}
- ourLog.debug("Processed metaAddOperation on {} in {}ms", new Object[] {theResourceId, w.getMillisAndRestart()});
+ ourLog.debug("Processed metaAddOperation on {} in {}ms", new Object[]{theResourceId, w.getMillisAndRestart()});
@SuppressWarnings("unchecked")
MT retVal = (MT) metaGetOperation(theMetaAdd.getClass(), theResourceId, theRequestDetails);
@@ -756,7 +755,7 @@ public abstract class BaseHapiFhirResourceDao extends B
myEntityManager.flush();
- ourLog.debug("Processed metaDeleteOperation on {} in {}ms", new Object[] {theResourceId.getValue(), w.getMillisAndRestart()});
+ ourLog.debug("Processed metaDeleteOperation on {} in {}ms", new Object[]{theResourceId.getValue(), w.getMillisAndRestart()});
@SuppressWarnings("unchecked")
MT retVal = (MT) metaGetOperation(theMetaDel.getClass(), theResourceId, theRequestDetails);
@@ -918,7 +917,7 @@ public abstract class BaseHapiFhirResourceDao extends B
return read(theId, theRequestDetails, false);
}
- @Override
+ @Override
public T read(IIdType theId, RequestDetails theRequestDetails, boolean theDeletedOk) {
validateResourceTypeAndThrowIllegalArgumentException(theId);
@@ -941,6 +940,9 @@ public abstract class BaseHapiFhirResourceDao extends B
}
}
+ // Interceptor broadcast: RESOURCE_MAY_BE_RETURNED
+ HookParams params = new HookParams().add(IBaseResource.class, retVal);
+ myInterceptorBroadcaster.callHooks(Pointcut.RESOURCE_MAY_BE_RETURNED, params);
ourLog.debug("Processed read on {} in {}ms", theId.getValue(), w.getMillisAndRestart());
return retVal;
@@ -1189,6 +1191,11 @@ public abstract class BaseHapiFhirResourceDao extends B
outcome.setId(id);
outcome.setResource(theResource);
outcome.setEntity(theEntity);
+
+ // Interceptor broadcast: RESOURCE_MAY_BE_RETURNED
+ HookParams params = new HookParams().add(IBaseResource.class, theResource);
+ myInterceptorBroadcaster.callHooks(Pointcut.RESOURCE_MAY_BE_RETURNED, params);
+
return outcome;
}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java
index 968a990cb81..e65d9a4fea8 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/SearchBuilder.java
@@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.dao;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -29,6 +29,9 @@ import ca.uhn.fhir.jpa.dao.index.IdHelperService;
import ca.uhn.fhir.jpa.dao.r4.MatchResourceUrlService;
import ca.uhn.fhir.jpa.entity.ResourceSearchView;
import ca.uhn.fhir.jpa.model.entity.*;
+import ca.uhn.fhir.jpa.model.interceptor.api.HookParams;
+import ca.uhn.fhir.jpa.model.interceptor.api.IInterceptorBroadcaster;
+import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.jpa.model.util.StringNormalizer;
import ca.uhn.fhir.jpa.searchparam.JpaRuntimeSearchParam;
import ca.uhn.fhir.jpa.searchparam.MatchUrlService;
@@ -137,6 +140,8 @@ public class SearchBuilder implements ISearchBuilder {
private MatchUrlService myMatchUrlService;
@Autowired
private IResourceIndexedCompositeStringUniqueDao myResourceIndexedCompositeStringUniqueDao;
+ @Autowired
+ private IInterceptorBroadcaster myInterceptorBroadcaster;
private List myAlsoIncludePids;
private CriteriaBuilder myBuilder;
private BaseHapiFhirDao> myCallingDao;
@@ -1922,6 +1927,10 @@ public class SearchBuilder implements ISearchBuilder {
}
}
+ // Interceptor broadcast: RESOURCE_MAY_BE_RETURNED
+ HookParams params = new HookParams().add(IBaseResource.class, resource);
+ myInterceptorBroadcaster.callHooks(Pointcut.RESOURCE_MAY_BE_RETURNED, params);
+
theResourceListToPopulate.set(index, resource);
}
}
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/VersionIndependentConcept.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/VersionIndependentConcept.java
index 578fb2d03de..2f5c683b935 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/VersionIndependentConcept.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/term/VersionIndependentConcept.java
@@ -9,9 +9,9 @@ package ca.uhn.fhir.jpa.term;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
- *
+ *
* http://www.apache.org/licenses/LICENSE-2.0
- *
+ *
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainDstu3.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainDstu3.java
index 5deefb44163..e8170a4732f 100644
--- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainDstu3.java
+++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/validation/JpaValidationSupportChainDstu3.java
@@ -23,7 +23,7 @@ package ca.uhn.fhir.jpa.validation;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
-import org.hl7.fhir.dstu3.hapi.validation.DefaultProfileValidationSupport;
+import org.hl7.fhir.dstu3.hapi.ctx.DefaultProfileValidationSupport;
import org.hl7.fhir.dstu3.hapi.validation.ValidationSupportChain;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java
index 9be5602552c..7c6ec741fb9 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/r4/FhirResourceDaoR4SearchNoFtTest.java
@@ -2350,6 +2350,31 @@ public class FhirResourceDaoR4SearchNoFtTest extends BaseJpaR4Test {
}
+ @Test
+ public void testSearchWithDateRange() {
+ SearchParameterMap sp = new SearchParameterMap();
+ sp.setLoadSynchronous(true);
+ sp.add(MedicationRequest.SP_INTENT, new TokenParam("FOO", "BAR"));
+ sp.setLastUpdated(new DateRangeParam()
+ .setUpperBound(new DateParam("le2019-02-22T17:50:00"))
+ .setLowerBound(new DateParam("ge2019-02-22T13:50:00")));
+ IBundleProvider retrieved = myMedicationRequestDao.search(sp);
+
+ List queries = CaptureQueriesListener
+ .getLastNQueries()
+ .stream()
+ .map(t -> t.getSql(true, true))
+ .collect(Collectors.toList());
+
+ ourLog.info("Queries:\n {}", queries.stream().findFirst());
+
+ String searchQuery = queries.get(0);
+ assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toUpperCase(), "HFJ_SPIDX_TOKEN"));
+ assertEquals(searchQuery, 1, StringUtils.countMatches(searchQuery.toUpperCase(), "LEFT OUTER JOIN"));
+ assertEquals(searchQuery, 2, StringUtils.countMatches(searchQuery.toUpperCase(), "AND RESOURCETA0_.RES_UPDATED"));
+ }
+
+
@Test
public void testSearchTokenParam() {
Patient patient = new Patient();
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java
index 3e4e064616e..5d2c2e612a0 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/BaseResourceProviderDstu2Test.java
@@ -74,7 +74,7 @@ public abstract class BaseResourceProviderDstu2Test extends BaseJpaDstu2Test {
ourRestServer.setResourceProviders((List)myResourceProviders);
- ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
+ ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(myFhirCtx));
ourRestServer.setPlainProviders(mySystemProvider);
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java
index f897c237199..751c0cab18c 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/BaseResourceProviderDstu3Test.java
@@ -92,7 +92,7 @@ public abstract class BaseResourceProviderDstu3Test extends BaseJpaDstu3Test {
ourRestServer.setResourceProviders((List) myResourceProviders);
- ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
+ ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(myFhirCtx));
myTerminologyUploaderProvider = myAppCtx.getBean(TerminologyUploaderProviderDstu3.class);
ourRestServer.setPlainProviders(mySystemProvider, myTerminologyUploaderProvider);
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java
index b08bafd86a8..9d6f92504f9 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/BaseResourceProviderR4Test.java
@@ -99,7 +99,7 @@ public abstract class BaseResourceProviderR4Test extends BaseJpaR4Test {
ourRestServer.setResourceProviders((List) myResourceProviders);
- ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
+ ourRestServer.getFhirContext().setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(myFhirCtx));
myTerminologyUploaderProvider = myAppCtx.getBean(TerminologyUploaderProviderR4.class);
ourGraphQLProvider = myAppCtx.getBean("myGraphQLProvider");
diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/CompositionDocumentR4Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/CompositionDocumentR4Test.java
index 15278e50b31..0efacbe93a6 100644
--- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/CompositionDocumentR4Test.java
+++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/r4/CompositionDocumentR4Test.java
@@ -1,33 +1,39 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.jpa.dao.DaoConfig;
+import ca.uhn.fhir.jpa.model.interceptor.api.HookParams;
+import ca.uhn.fhir.jpa.model.interceptor.api.IAnonymousLambdaHook;
+import ca.uhn.fhir.jpa.model.interceptor.api.Pointcut;
import ca.uhn.fhir.parser.StrictErrorHandler;
import ca.uhn.fhir.rest.api.EncodingEnum;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
-import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
-
+import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.*;
-import org.hl7.fhir.r4.model.codesystems.EncounterStatus;
-import org.hl7.fhir.r4.model.codesystems.ObservationStatus;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mockito;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.stream.Collectors;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasItems;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
public class CompositionDocumentR4Test extends BaseResourceProviderR4Test {
@@ -38,6 +44,8 @@ public class CompositionDocumentR4Test extends BaseResourceProviderR4Test {
private String encId;
private String listId;
private String compId;
+ @Captor
+ private ArgumentCaptor myHookParamsCaptor;
@Before
public void beforeDisableResultReuse() {
@@ -49,6 +57,7 @@ public class CompositionDocumentR4Test extends BaseResourceProviderR4Test {
public void after() throws Exception {
super.after();
+ myInterceptorRegistry.clearAnonymousHookForUnitTest();
myDaoConfig.setReuseCachedSearchResultsForMillis(new DaoConfig().getReuseCachedSearchResultsForMillis());
}
@@ -77,7 +86,7 @@ public class CompositionDocumentR4Test extends BaseResourceProviderR4Test {
ListResource listResource = new ListResource();
ArrayList myObs = new ArrayList<>();
- myObsIds = new ArrayList();
+ myObsIds = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Observation obs = new Observation();
obs.getSubject().setReference(patId);
@@ -125,14 +134,38 @@ public class CompositionDocumentR4Test extends BaseResourceProviderR4Test {
assertThat(actual, hasItems(myObsIds.toArray(new String[0])));
}
- private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOException, ClientProtocolException {
+ @Test
+ public void testInterceptorHookIsCalledForAllContents_RESOURCE_MAY_BE_RETURNED() throws IOException {
+
+ IAnonymousLambdaHook pointcut = mock(IAnonymousLambdaHook.class);
+ myInterceptorRegistry.registerAnonymousHookForUnitTest(Pointcut.RESOURCE_MAY_BE_RETURNED, pointcut);
+
+ String theUrl = ourServerBase + "/" + compId + "/$document?_format=json";
+ fetchBundle(theUrl, EncodingEnum.JSON);
+
+ Mockito.verify(pointcut, times(10)).invoke(myHookParamsCaptor.capture());
+
+ List returnedClasses = myHookParamsCaptor
+ .getAllValues()
+ .stream()
+ .map(t -> t.get(IBaseResource.class, 0))
+ .map(t -> t.getClass().getSimpleName())
+ .collect(Collectors.toList());
+
+ ourLog.info("Returned classes: {}", returnedClasses);
+
+ assertThat(returnedClasses, hasItem("Composition"));
+ assertThat(returnedClasses, hasItem("Organization"));
+ }
+
+ private Bundle fetchBundle(String theUrl, EncodingEnum theEncoding) throws IOException {
Bundle bundle;
HttpGet get = new HttpGet(theUrl);
try (CloseableHttpResponse resp = ourHttpClient.execute(get)) {
String resourceString = IOUtils.toString(resp.getEntity().getContent(), Charsets.UTF_8);
bundle = theEncoding.newParser(myFhirCtx).parseResource(Bundle.class, resourceString);
- }
+ }
return bundle;
}
diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java
index 38506ddd976..3c4643f0bb7 100644
--- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java
+++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemo.java
@@ -109,7 +109,7 @@ public class JpaServerDemo extends RestfulServer {
* This server tries to dynamically generate narratives
*/
FhirContext ctx = getFhirContext();
- ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
+ ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
/*
* Default to JSON and pretty printing
diff --git a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java
index 3c802e6cb10..0d4121947d9 100644
--- a/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java
+++ b/hapi-fhir-jpaserver-example/src/main/java/ca/uhn/fhir/jpa/demo/JpaServerDemoDstu2.java
@@ -108,7 +108,7 @@ public class JpaServerDemoDstu2 extends RestfulServer {
* This server tries to dynamically generate narratives
*/
FhirContext ctx = getFhirContext();
- ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator());
+ ctx.setNarrativeGenerator(new DefaultThymeleafNarrativeGenerator(getFhirContext()));
/*
* Default to JSON and pretty printing
diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java
index eab60633a37..45c05f6a8a3 100644
--- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java
+++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/api/Pointcut.java
@@ -29,6 +29,11 @@ import java.util.List;
*/
public enum Pointcut {
+ /**
+ * This pointcut will be called once when a given interceptor is registered
+ */
+ REGISTERED,
+
/**
* Invoked whenever a persisted resource has been modified and is being submitted to the
* subscription processing pipeline. This method is called before the resource is placed
@@ -330,7 +335,32 @@ public enum Pointcut {
* Hooks should return void.
*
*/
- OP_PRESTORAGE_RESOURCE_UPDATED("org.hl7.fhir.instance.model.api.IBaseResource", "org.hl7.fhir.instance.model.api.IBaseResource");
+ OP_PRESTORAGE_RESOURCE_UPDATED("org.hl7.fhir.instance.model.api.IBaseResource", "org.hl7.fhir.instance.model.api.IBaseResource"),
+
+ /**
+ * Invoked when a resource may be returned to the user, whether as a part of a READ,
+ * a SEARCH, or even as the response to a CREATE/UPDATE, etc.
+ *
+ * This hook is invoked when a resource has been loaded by the storage engine and
+ * is being returned to the HTTP stack for response. This is not a guarantee that the
+ * client will ultimately see it, since filters/headers/etc may affect what
+ * is returned but if a resource is loaded it is likely to be used.
+ * Note also that caching may affect whether this pointcut is invoked.
+ *
+ *
+ * Hooks will have access to the contents of the resource being returned
+ * and may choose to make modifications. These changes will be reflected in
+ * returned resource but have no effect on storage.
+ *
+ * Hooks may accept the following parameters:
+ *
+ *
org.hl7.fhir.instance.model.api.IBaseResource (the resource being returned)
+ *
+ *
+ * Hooks should return void.
+ *
+ */
+ RESOURCE_MAY_BE_RETURNED("org.hl7.fhir.instance.model.api.IBaseResource");
private final List myParameterTypes;
diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorService.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorService.java
index 26e2831d361..d62f0a25417 100644
--- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorService.java
+++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorService.java
@@ -25,6 +25,7 @@ import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Sets;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
@@ -92,11 +93,26 @@ public class InterceptorService implements IInterceptorRegistry, IInterceptorBro
Class> interceptorClass = theInterceptor.getClass();
int typeOrder = determineOrder(interceptorClass);
- if (!scanInterceptorForHookMethodsAndAddThem(theInterceptor, typeOrder)) {
+ List addedInvokers = scanInterceptorForHookMethods(theInterceptor, typeOrder);
+ if (addedInvokers.isEmpty()) {
return false;
}
+ // Invoke the REGISTERED pointcut for any added hooks
+ addedInvokers.stream()
+ .filter(t -> t.getPointcuts().contains(Pointcut.REGISTERED))
+ .forEach(t -> t.invoke(new HookParams()));
+
+ // Register the interceptor and its various hooks
myInterceptors.add(theInterceptor);
+ for (HookInvoker nextAddedHook : addedInvokers) {
+ for (Pointcut nextPointcut : nextAddedHook.getPointcuts()) {
+ if (nextPointcut.equals(Pointcut.REGISTERED)) {
+ continue;
+ }
+ myInvokers.put(nextPointcut, nextAddedHook);
+ }
+ }
// Make sure we're always sorted according to the order declared in
// @Order
@@ -110,27 +126,25 @@ public class InterceptorService implements IInterceptorRegistry, IInterceptorBro
}
}
- private boolean scanInterceptorForHookMethodsAndAddThem(Object theInterceptor, int theTypeOrder) {
- boolean retVal = false;
+ /**
+ * @return Returns a list of any added invokers
+ */
+ private List scanInterceptorForHookMethods(Object theInterceptor, int theTypeOrder) {
+ ArrayList retVal = new ArrayList<>();
for (Method nextMethod : theInterceptor.getClass().getDeclaredMethods()) {
Hook hook = AnnotationUtils.findAnnotation(nextMethod, Hook.class);
if (hook != null) {
-
int methodOrder = theTypeOrder;
Order methodOrderAnnotation = AnnotationUtils.findAnnotation(nextMethod, Order.class);
if (methodOrderAnnotation != null) {
methodOrder = methodOrderAnnotation.value();
}
- HookInvoker invoker = new HookInvoker(hook, theInterceptor, nextMethod, methodOrder);
- for (Pointcut nextPointcut : hook.value()) {
- myInvokers.put(nextPointcut, invoker);
- }
-
- retVal = true;
+ retVal.add(new HookInvoker(hook, theInterceptor, nextMethod, methodOrder));
}
}
+
return retVal;
}
@@ -300,12 +314,14 @@ public class InterceptorService implements IInterceptorRegistry, IInterceptorBro
private final Method myMethod;
private final Class>[] myParameterTypes;
private final int[] myParameterIndexes;
+ private final Set myPointcuts;
/**
* Constructor
*/
private HookInvoker(Hook theHook, @Nonnull Object theInterceptor, @Nonnull Method theHookMethod, int theOrder) {
super(theInterceptor, theOrder);
+ myPointcuts = Collections.unmodifiableSet(Sets.newHashSet(theHook.value()));
myParameterTypes = theHookMethod.getParameterTypes();
myMethod = theHookMethod;
@@ -325,6 +341,10 @@ public class InterceptorService implements IInterceptorRegistry, IInterceptorBro
}
}
+ public Set getPointcuts() {
+ return myPointcuts;
+ }
+
/**
* @return Returns true/false if the hook method returns a boolean, returns true otherwise
*/
@@ -352,7 +372,7 @@ public class InterceptorService implements IInterceptorRegistry, IInterceptorBro
if (targetException instanceof RuntimeException) {
throw ((RuntimeException) targetException);
} else {
- throw new InternalErrorException(targetException);
+ throw new InternalErrorException("Failure invoking interceptor for pointcut(s) " + getPointcuts(), targetException);
}
} catch (Exception e) {
throw new InternalErrorException(e);
diff --git a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorServiceTest.java b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorServiceTest.java
index eede79c7d30..d7878b0582d 100644
--- a/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorServiceTest.java
+++ b/hapi-fhir-jpaserver-model/src/test/java/ca/uhn/fhir/jpa/model/interceptor/executor/InterceptorServiceTest.java
@@ -1,6 +1,7 @@
package ca.uhn.fhir.jpa.model.interceptor.executor;
import ca.uhn.fhir.jpa.model.interceptor.api.*;
+import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Patient;
import org.junit.Before;
@@ -40,6 +41,21 @@ public class InterceptorServiceTest {
@Autowired
private MyTestInterceptorManual myInterceptorManual;
+ @Test
+ public void testRegisterHookFails() {
+ int initialSize = myInterceptorRegistry.getGlobalInterceptorsForUnitTest().size();
+
+ try {
+ myInterceptorRegistry.registerInterceptor(new InterceptorThatFailsOnRegister());
+ fail();
+ } catch (InternalErrorException e) {
+ // good
+ }
+
+ assertEquals(initialSize, myInterceptorRegistry.getGlobalInterceptorsForUnitTest().size());
+
+ }
+
@Test
public void testGlobalInterceptorsAreFound() {
List