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