
001package ca.uhn.fhir.narrative2; 002 003/*- 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.context.ConfigurationException; 024import ca.uhn.fhir.context.FhirContext; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; 027import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 028import com.google.common.base.Charsets; 029import org.apache.commons.io.IOUtils; 030import org.apache.commons.lang3.StringUtils; 031import org.apache.commons.lang3.Validate; 032import org.hl7.fhir.instance.model.api.IBase; 033import org.hl7.fhir.instance.model.api.IBaseResource; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036 037import java.io.File; 038import java.io.FileInputStream; 039import java.io.IOException; 040import java.io.InputStream; 041import java.io.StringReader; 042import java.util.ArrayList; 043import java.util.Arrays; 044import java.util.Collection; 045import java.util.Collections; 046import java.util.EnumSet; 047import java.util.HashMap; 048import java.util.List; 049import java.util.Map; 050import java.util.Properties; 051import java.util.stream.Collectors; 052 053import static org.apache.commons.lang3.StringUtils.isNotBlank; 054 055public class NarrativeTemplateManifest implements INarrativeTemplateManifest { 056 private static final Logger ourLog = LoggerFactory.getLogger(NarrativeTemplateManifest.class); 057 058 private final Map<String, List<NarrativeTemplate>> myResourceTypeToTemplate; 059 private final Map<String, List<NarrativeTemplate>> myDatatypeToTemplate; 060 private final Map<String, List<NarrativeTemplate>> myNameToTemplate; 061 private final Map<String, List<NarrativeTemplate>> myClassToTemplate; 062 private final int myTemplateCount; 063 064 private NarrativeTemplateManifest(Collection<NarrativeTemplate> theTemplates) { 065 Map<String, List<NarrativeTemplate>> resourceTypeToTemplate = new HashMap<>(); 066 Map<String, List<NarrativeTemplate>> datatypeToTemplate = new HashMap<>(); 067 Map<String, List<NarrativeTemplate>> nameToTemplate = new HashMap<>(); 068 Map<String, List<NarrativeTemplate>> classToTemplate = new HashMap<>(); 069 070 for (NarrativeTemplate nextTemplate : theTemplates) { 071 nameToTemplate.computeIfAbsent(nextTemplate.getTemplateName(), t -> new ArrayList<>()).add(nextTemplate); 072 for (String nextResourceType : nextTemplate.getAppliesToResourceTypes()) { 073 resourceTypeToTemplate.computeIfAbsent(nextResourceType.toUpperCase(), t -> new ArrayList<>()).add(nextTemplate); 074 } 075 for (String nextDataType : nextTemplate.getAppliesToDataTypes()) { 076 datatypeToTemplate.computeIfAbsent(nextDataType.toUpperCase(), t -> new ArrayList<>()).add(nextTemplate); 077 } 078 for (Class<? extends IBase> nextAppliesToClass : nextTemplate.getAppliesToClasses()) { 079 classToTemplate.computeIfAbsent(nextAppliesToClass.getName(), t -> new ArrayList<>()).add(nextTemplate); 080 } 081 } 082 083 myTemplateCount = theTemplates.size(); 084 myClassToTemplate = makeImmutable(classToTemplate); 085 myNameToTemplate = makeImmutable(nameToTemplate); 086 myResourceTypeToTemplate = makeImmutable(resourceTypeToTemplate); 087 myDatatypeToTemplate = makeImmutable(datatypeToTemplate); 088 } 089 090 public int getNamedTemplateCount() { 091 return myTemplateCount; 092 } 093 094 @Override 095 public List<INarrativeTemplate> getTemplateByResourceName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theResourceName) { 096 return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate); 097 } 098 099 @Override 100 public List<INarrativeTemplate> getTemplateByName(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, String theName) { 101 return getFromMap(theStyles, theName, myNameToTemplate); 102 } 103 104 @Override 105 public List<INarrativeTemplate> getTemplateByElement(FhirContext theFhirContext, EnumSet<TemplateTypeEnum> theStyles, IBase theElement) { 106 List<INarrativeTemplate> retVal = getFromMap(theStyles, theElement.getClass().getName(), myClassToTemplate); 107 if (retVal.isEmpty()) { 108 if (theElement instanceof IBaseResource) { 109 String resourceName = theFhirContext.getResourceDefinition((IBaseResource) theElement).getName(); 110 retVal = getTemplateByResourceName(theFhirContext, theStyles, resourceName); 111 } else { 112 String datatypeName = theFhirContext.getElementDefinition(theElement.getClass()).getName(); 113 retVal = getFromMap(theStyles, datatypeName.toUpperCase(), myDatatypeToTemplate); 114 } 115 } 116 return retVal; 117 } 118 119 public static NarrativeTemplateManifest forManifestFileLocation(String... thePropertyFilePaths) throws IOException { 120 return forManifestFileLocation(Arrays.asList(thePropertyFilePaths)); 121 } 122 123 public static NarrativeTemplateManifest forManifestFileLocation(Collection<String> thePropertyFilePaths) throws IOException { 124 ourLog.debug("Loading narrative properties file(s): {}", thePropertyFilePaths); 125 126 List<String> manifestFileContents = new ArrayList<>(thePropertyFilePaths.size()); 127 for (String next : thePropertyFilePaths) { 128 String resource = loadResource(next); 129 manifestFileContents.add(resource); 130 } 131 132 return forManifestFileContents(manifestFileContents); 133 } 134 135 public static NarrativeTemplateManifest forManifestFileContents(String... theResources) throws IOException { 136 return forManifestFileContents(Arrays.asList(theResources)); 137 } 138 139 public static NarrativeTemplateManifest forManifestFileContents(Collection<String> theResources) throws IOException { 140 List<NarrativeTemplate> templates = new ArrayList<>(); 141 for (String next : theResources) { 142 templates.addAll(loadProperties(next)); 143 } 144 return new NarrativeTemplateManifest(templates); 145 } 146 147 private static Collection<NarrativeTemplate> loadProperties(String theManifestText) throws IOException { 148 Map<String, NarrativeTemplate> nameToTemplate = new HashMap<>(); 149 150 Properties file = new Properties(); 151 152 file.load(new StringReader(theManifestText)); 153 for (Object nextKeyObj : file.keySet()) { 154 String nextKey = (String) nextKeyObj; 155 Validate.isTrue(StringUtils.countMatches(nextKey, ".") == 1, "Invalid narrative property file key: %s", nextKey); 156 String name = nextKey.substring(0, nextKey.indexOf('.')); 157 Validate.notBlank(name, "Invalid narrative property file key: %s", nextKey); 158 159 NarrativeTemplate nextTemplate = nameToTemplate.computeIfAbsent(name, t -> new NarrativeTemplate().setTemplateName(name)); 160 161 if (nextKey.endsWith(".class")) { 162 String className = file.getProperty(nextKey); 163 if (isNotBlank(className)) { 164 try { 165 nextTemplate.addAppliesToClass((Class<? extends IBase>) Class.forName(className)); 166 } catch (ClassNotFoundException theE) { 167 throw new InternalErrorException(Msg.code(1867) + "Could not find class " + className + " declared in narative manifest"); 168 } 169 } 170 } else if (nextKey.endsWith(".profile")) { 171 String profile = file.getProperty(nextKey); 172 if (isNotBlank(profile)) { 173 nextTemplate.addAppliesToProfile(profile); 174 } 175 } else if (nextKey.endsWith(".resourceType")) { 176 String resourceType = file.getProperty(nextKey); 177 Arrays 178 .stream(resourceType.split(",")) 179 .map(t -> t.trim()) 180 .filter(t -> isNotBlank(t)) 181 .forEach(t -> nextTemplate.addAppliesToResourceType(t)); 182 } else if (nextKey.endsWith(".dataType")) { 183 String dataType = file.getProperty(nextKey); 184 Arrays 185 .stream(dataType.split(",")) 186 .map(t -> t.trim()) 187 .filter(t -> isNotBlank(t)) 188 .forEach(t -> nextTemplate.addAppliesToDatatype(t)); 189 } else if (nextKey.endsWith(".style")) { 190 String templateTypeName = file.getProperty(nextKey).toUpperCase(); 191 TemplateTypeEnum templateType = TemplateTypeEnum.valueOf(templateTypeName); 192 nextTemplate.setTemplateType(templateType); 193 } else if (nextKey.endsWith(".contextPath")) { 194 String contextPath = file.getProperty(nextKey); 195 nextTemplate.setContextPath(contextPath); 196 } else if (nextKey.endsWith(".narrative")) { 197 String narrativePropName = name + ".narrative"; 198 String narrativeName = file.getProperty(narrativePropName); 199 if (StringUtils.isNotBlank(narrativeName)) { 200 nextTemplate.setTemplateFileName(narrativeName); 201 } 202 } else if (nextKey.endsWith(".title")) { 203 ourLog.debug("Ignoring title property as narrative generator no longer generates titles: {}", nextKey); 204 } else { 205 throw new ConfigurationException(Msg.code(1868) + "Invalid property name: " + nextKey 206 + " - the key must end in one of the expected extensions " 207 + "'.profile', '.resourceType', '.dataType', '.style', '.contextPath', '.narrative', '.title'"); 208 } 209 210 } 211 212 return nameToTemplate.values(); 213 } 214 215 static String loadResource(String name) throws IOException { 216 if (name.startsWith("classpath:")) { 217 String cpName = name.substring("classpath:".length()); 218 try (InputStream resource = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream(cpName)) { 219 if (resource == null) { 220 try (InputStream resource2 = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream("/" + cpName)) { 221 if (resource2 == null) { 222 throw new IOException(Msg.code(1869) + "Can not find '" + cpName + "' on classpath"); 223 } 224 return IOUtils.toString(resource2, Charsets.UTF_8); 225 } 226 } 227 return IOUtils.toString(resource, Charsets.UTF_8); 228 } 229 } else if (name.startsWith("file:")) { 230 File file = new File(name.substring("file:".length())); 231 if (file.exists() == false) { 232 throw new IOException(Msg.code(1870) + "File not found: " + file.getAbsolutePath()); 233 } 234 try (FileInputStream inputStream = new FileInputStream(file)) { 235 return IOUtils.toString(inputStream, Charsets.UTF_8); 236 } 237 } else { 238 throw new IOException(Msg.code(1871) + "Invalid resource name: '" + name + "' (must start with classpath: or file: )"); 239 } 240 } 241 242 private static <T> List<INarrativeTemplate> getFromMap(EnumSet<TemplateTypeEnum> theStyles, T theKey, Map<T, List<NarrativeTemplate>> theMap) { 243 return theMap 244 .getOrDefault(theKey, Collections.emptyList()) 245 .stream() 246 .filter(t -> theStyles.contains(t.getTemplateType())) 247 .collect(Collectors.toList()); 248 } 249 250 private static <T> Map<T, List<NarrativeTemplate>> makeImmutable(Map<T, List<NarrativeTemplate>> theStyleToResourceTypeToTemplate) { 251 theStyleToResourceTypeToTemplate.replaceAll((key, value) -> Collections.unmodifiableList(value)); 252 return Collections.unmodifiableMap(theStyleToResourceTypeToTemplate); 253 } 254 255}