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