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