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