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