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