001/*- 002 * #%L 003 * HAPI FHIR - Core Library 004 * %% 005 * Copyright (C) 2014 - 2024 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 return getFromMap(theStyles, theResourceName.toUpperCase(), myResourceTypeToTemplate, theProfiles); 112 } 113 114 @Override 115 public List<INarrativeTemplate> getTemplateByName( 116 @Nonnull FhirContext theFhirContext, 117 @Nonnull EnumSet<TemplateTypeEnum> theStyles, 118 @Nonnull String theName) { 119 return getFromMap(theStyles, theName, myNameToTemplate, Collections.emptyList()); 120 } 121 122 @Override 123 public List<INarrativeTemplate> getTemplateByFragmentName( 124 @Nonnull FhirContext theFhirContext, 125 @Nonnull EnumSet<TemplateTypeEnum> theStyles, 126 @Nonnull String theFragmentName) { 127 return getFromMap(theStyles, theFragmentName, myFragmentNameToTemplate, Collections.emptyList()); 128 } 129 130 @SuppressWarnings("PatternVariableCanBeUsed") 131 @Override 132 public List<INarrativeTemplate> getTemplateByElement( 133 @Nonnull FhirContext theFhirContext, 134 @Nonnull EnumSet<TemplateTypeEnum> theStyles, 135 @Nonnull IBase theElement) { 136 List<INarrativeTemplate> retVal = Collections.emptyList(); 137 138 if (theElement instanceof IBaseResource) { 139 IBaseResource resource = (IBaseResource) theElement; 140 String resourceName = theFhirContext.getResourceDefinition(resource).getName(); 141 List<String> profiles = resource.getMeta().getProfile().stream() 142 .filter(Objects::nonNull) 143 .map(IPrimitiveType::getValueAsString) 144 .filter(StringUtils::isNotBlank) 145 .collect(Collectors.toList()); 146 retVal = getTemplateByResourceName(theFhirContext, theStyles, resourceName, profiles); 147 } 148 149 if (retVal.isEmpty()) { 150 retVal = getFromMap(theStyles, theElement.getClass().getName(), myClassToTemplate, Collections.emptyList()); 151 } 152 153 if (retVal.isEmpty()) { 154 String datatypeName = 155 theFhirContext.getElementDefinition(theElement.getClass()).getName(); 156 retVal = getFromMap(theStyles, datatypeName.toUpperCase(), myDatatypeToTemplate, Collections.emptyList()); 157 } 158 return retVal; 159 } 160 161 public static NarrativeTemplateManifest forManifestFileLocation(String... thePropertyFilePaths) { 162 return forManifestFileLocation(Arrays.asList(thePropertyFilePaths)); 163 } 164 165 public static NarrativeTemplateManifest forManifestFileLocation(Collection<String> thePropertyFilePaths) { 166 ourLog.debug("Loading narrative properties file(s): {}", thePropertyFilePaths); 167 168 List<String> manifestFileContents = new ArrayList<>(thePropertyFilePaths.size()); 169 for (String next : thePropertyFilePaths) { 170 String resource = loadResource(next); 171 manifestFileContents.add(resource); 172 } 173 174 return forManifestFileContents(manifestFileContents); 175 } 176 177 public static NarrativeTemplateManifest forManifestFileContents(String... theResources) { 178 return forManifestFileContents(Arrays.asList(theResources)); 179 } 180 181 public static NarrativeTemplateManifest forManifestFileContents(Collection<String> theResources) { 182 try { 183 List<NarrativeTemplate> templates = new ArrayList<>(); 184 for (String next : theResources) { 185 templates.addAll(loadProperties(next)); 186 } 187 return new NarrativeTemplateManifest(templates); 188 } catch (IOException e) { 189 throw new InternalErrorException(Msg.code(1808) + e); 190 } 191 } 192 193 @SuppressWarnings("unchecked") 194 private static Collection<NarrativeTemplate> loadProperties(String theManifestText) throws IOException { 195 Map<String, NarrativeTemplate> nameToTemplate = new HashMap<>(); 196 197 Properties file = new Properties(); 198 199 file.load(new StringReader(theManifestText)); 200 for (Object nextKeyObj : file.keySet()) { 201 String nextKey = (String) nextKeyObj; 202 Validate.isTrue( 203 StringUtils.countMatches(nextKey, ".") == 1, "Invalid narrative property file key: %s", nextKey); 204 String name = nextKey.substring(0, nextKey.indexOf('.')); 205 Validate.notBlank(name, "Invalid narrative property file key: %s", nextKey); 206 207 NarrativeTemplate nextTemplate = 208 nameToTemplate.computeIfAbsent(name, t -> new NarrativeTemplate().setTemplateName(name)); 209 210 if (nextKey.endsWith(".class")) { 211 String className = file.getProperty(nextKey); 212 if (isNotBlank(className)) { 213 try { 214 nextTemplate.addAppliesToClass((Class<? extends IBase>) Class.forName(className)); 215 } catch (ClassNotFoundException theE) { 216 throw new InternalErrorException(Msg.code(1867) + "Could not find class " + className 217 + " declared in narrative manifest"); 218 } 219 } 220 } else if (nextKey.endsWith(".profile")) { 221 String profile = file.getProperty(nextKey); 222 if (isNotBlank(profile)) { 223 nextTemplate.addAppliesToProfile(profile); 224 } 225 } else if (nextKey.endsWith(".resourceType")) { 226 String resourceType = file.getProperty(nextKey); 227 parseValuesAndAddToMap(resourceType, nextTemplate::addAppliesToResourceType); 228 } else if (nextKey.endsWith(".fragmentName")) { 229 String resourceType = file.getProperty(nextKey); 230 parseValuesAndAddToMap(resourceType, nextTemplate::addAppliesToFragmentName); 231 } else if (nextKey.endsWith(".dataType")) { 232 String dataType = file.getProperty(nextKey); 233 parseValuesAndAddToMap(dataType, nextTemplate::addAppliesToDatatype); 234 } else if (nextKey.endsWith(".style")) { 235 String templateTypeName = file.getProperty(nextKey).toUpperCase(); 236 TemplateTypeEnum templateType = TemplateTypeEnum.valueOf(templateTypeName); 237 nextTemplate.setTemplateType(templateType); 238 } else if (nextKey.endsWith(".contextPath")) { 239 String contextPath = file.getProperty(nextKey); 240 nextTemplate.setContextPath(contextPath); 241 } else if (nextKey.endsWith(".narrative")) { 242 String narrativePropName = name + ".narrative"; 243 String narrativeName = file.getProperty(narrativePropName); 244 if (StringUtils.isNotBlank(narrativeName)) { 245 nextTemplate.setTemplateFileName(narrativeName); 246 } 247 } else if (nextKey.endsWith(".title")) { 248 ourLog.debug("Ignoring title property as narrative generator no longer generates titles: {}", nextKey); 249 } else { 250 throw new ConfigurationException(Msg.code(1868) + "Invalid property name: " + nextKey 251 + " - the key must end in one of the expected extensions " 252 + "'.profile', '.resourceType', '.dataType', '.style', '.contextPath', '.narrative', '.title'"); 253 } 254 } 255 256 return nameToTemplate.values(); 257 } 258 259 private static void parseValuesAndAddToMap(String resourceType, Consumer<String> addAppliesToResourceType) { 260 Arrays.stream(resourceType.split(",")) 261 .map(String::trim) 262 .filter(StringUtils::isNotBlank) 263 .forEach(addAppliesToResourceType); 264 } 265 266 static String loadResource(String theName) { 267 if (theName.startsWith("classpath:")) { 268 return ClasspathUtil.loadResource(theName); 269 } else if (theName.startsWith("file:")) { 270 File file = new File(theName.substring("file:".length())); 271 if (file.exists() == false) { 272 throw new InternalErrorException(Msg.code(1870) + "File not found: " + file.getAbsolutePath()); 273 } 274 try (FileInputStream inputStream = new FileInputStream(file)) { 275 return IOUtils.toString(inputStream, Charsets.UTF_8); 276 } catch (IOException e) { 277 throw new InternalErrorException(Msg.code(1869) + e.getMessage(), e); 278 } 279 } else { 280 throw new InternalErrorException( 281 Msg.code(1871) + "Invalid resource name: '" + theName + "' (must start with classpath: or file: )"); 282 } 283 } 284 285 private static <T> List<INarrativeTemplate> getFromMap( 286 EnumSet<TemplateTypeEnum> theStyles, 287 T theKey, 288 ListMultimap<T, NarrativeTemplate> theMap, 289 Collection<String> theProfiles) { 290 return theMap.get(theKey).stream() 291 .filter(t -> theStyles.contains(t.getTemplateType())) 292 .filter(t -> theProfiles.isEmpty() 293 || t.getAppliesToProfiles().stream().anyMatch(theProfiles::contains)) 294 .collect(Collectors.toList()); 295 } 296}