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