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