001package ca.uhn.fhir.narrative2; 002 003/*- 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2019 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(".class")) { 159 String className = file.getProperty(nextKey); 160 Class<? extends IBase> clazz; 161 try { 162 clazz = (Class<? extends IBase>) Class.forName(className); 163 } catch (ClassNotFoundException e) { 164 ourLog.debug("Unknown datatype class '{}' identified in manifest", name); 165 clazz = null; 166 } 167 if (clazz != null) { 168 nextTemplate.addAppliesToResourceClass(clazz); 169 } 170 } else if (nextKey.endsWith(".style")) { 171 String templateTypeName = file.getProperty(nextKey).toUpperCase(); 172 TemplateTypeEnum templateType = TemplateTypeEnum.valueOf(templateTypeName); 173 nextTemplate.setTemplateType(templateType); 174 } else if (nextKey.endsWith(".contextPath")) { 175 String contextPath = file.getProperty(nextKey); 176 nextTemplate.setContextPath(contextPath); 177 } else if (nextKey.endsWith(".narrative")) { 178 String narrativePropName = name + ".narrative"; 179 String narrativeName = file.getProperty(narrativePropName); 180 if (StringUtils.isNotBlank(narrativeName)) { 181 nextTemplate.setTemplateFileName(narrativeName); 182 } 183 } else if (nextKey.endsWith(".title")) { 184 ourLog.debug("Ignoring title property as narrative generator no longer generates titles: {}", nextKey); 185 } else { 186 throw new ConfigurationException("Invalid property name: " + nextKey); 187 } 188 189 } 190 191 return nameToTemplate.values(); 192 } 193 194 static String loadResource(String name) throws IOException { 195 if (name.startsWith("classpath:")) { 196 String cpName = name.substring("classpath:".length()); 197 try (InputStream resource = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream(cpName)) { 198 if (resource == null) { 199 try (InputStream resource2 = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream("/" + cpName)) { 200 if (resource2 == null) { 201 throw new IOException("Can not find '" + cpName + "' on classpath"); 202 } 203 return IOUtils.toString(resource2, Charsets.UTF_8); 204 } 205 } 206 return IOUtils.toString(resource, Charsets.UTF_8); 207 } 208 } else if (name.startsWith("file:")) { 209 File file = new File(name.substring("file:".length())); 210 if (file.exists() == false) { 211 throw new IOException("File not found: " + file.getAbsolutePath()); 212 } 213 try (FileInputStream inputStream = new FileInputStream(file)) { 214 return IOUtils.toString(inputStream, Charsets.UTF_8); 215 } 216 } else { 217 throw new IOException("Invalid resource name: '" + name + "' (must start with classpath: or file: )"); 218 } 219 } 220 221 private static <T> List<INarrativeTemplate> getFromMap(EnumSet<TemplateTypeEnum> theStyles, T theKey, Map<T, List<NarrativeTemplate>> theMap) { 222 return theMap 223 .getOrDefault(theKey, Collections.emptyList()) 224 .stream() 225 .filter(t->theStyles.contains(t.getTemplateType())) 226 .collect(Collectors.toList()); 227 } 228 229 private static <T> Map<T, List<NarrativeTemplate>> makeImmutable(Map<T, List<NarrativeTemplate>> theStyleToResourceTypeToTemplate) { 230 theStyleToResourceTypeToTemplate.replaceAll((key, value) -> Collections.unmodifiableList(value)); 231 return Collections.unmodifiableMap(theStyleToResourceTypeToTemplate); 232 } 233 234}