001package ca.uhn.fhir.narrative2;
002
003/*-
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2020 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(".style")) {
159                                String templateTypeName = file.getProperty(nextKey).toUpperCase();
160                                TemplateTypeEnum templateType = TemplateTypeEnum.valueOf(templateTypeName);
161                                nextTemplate.setTemplateType(templateType);
162                        } else if (nextKey.endsWith(".contextPath")) {
163                                String contextPath = file.getProperty(nextKey);
164                                nextTemplate.setContextPath(contextPath);
165                        } else if (nextKey.endsWith(".narrative")) {
166                                String narrativePropName = name + ".narrative";
167                                String narrativeName = file.getProperty(narrativePropName);
168                                if (StringUtils.isNotBlank(narrativeName)) {
169                                        nextTemplate.setTemplateFileName(narrativeName);
170                                }
171                        } else if (nextKey.endsWith(".title")) {
172                                ourLog.debug("Ignoring title property as narrative generator no longer generates titles: {}", nextKey);
173                        } else {
174                                throw new ConfigurationException("Invalid property name: " + nextKey 
175                                                                                                                        + " - the key must end in one of the expected extensions "
176                                                                                                                        + "'.profile', '.resourceType', '.dataType', '.style', '.contextPath', '.narrative', '.title'");
177                        }
178
179                }
180
181                return nameToTemplate.values();
182        }
183
184        static String loadResource(String name) throws IOException {
185                if (name.startsWith("classpath:")) {
186                        String cpName = name.substring("classpath:".length());
187                        try (InputStream resource = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream(cpName)) {
188                                if (resource == null) {
189                                        try (InputStream resource2 = DefaultThymeleafNarrativeGenerator.class.getResourceAsStream("/" + cpName)) {
190                                                if (resource2 == null) {
191                                                        throw new IOException("Can not find '" + cpName + "' on classpath");
192                                                }
193                                                return IOUtils.toString(resource2, Charsets.UTF_8);
194                                        }
195                                }
196                                return IOUtils.toString(resource, Charsets.UTF_8);
197                        }
198                } else if (name.startsWith("file:")) {
199                        File file = new File(name.substring("file:".length()));
200                        if (file.exists() == false) {
201                                throw new IOException("File not found: " + file.getAbsolutePath());
202                        }
203                        try (FileInputStream inputStream = new FileInputStream(file)) {
204                                return IOUtils.toString(inputStream, Charsets.UTF_8);
205                        }
206                } else {
207                        throw new IOException("Invalid resource name: '" + name + "' (must start with classpath: or file: )");
208                }
209        }
210
211        private static <T> List<INarrativeTemplate> getFromMap(EnumSet<TemplateTypeEnum> theStyles, T theKey, Map<T, List<NarrativeTemplate>> theMap) {
212                return theMap
213                        .getOrDefault(theKey, Collections.emptyList())
214                        .stream()
215                        .filter(t->theStyles.contains(t.getTemplateType()))
216                        .collect(Collectors.toList());
217        }
218
219        private static <T> Map<T, List<NarrativeTemplate>> makeImmutable(Map<T, List<NarrativeTemplate>> theStyleToResourceTypeToTemplate) {
220                theStyleToResourceTypeToTemplate.replaceAll((key, value) -> Collections.unmodifiableList(value));
221                return Collections.unmodifiableMap(theStyleToResourceTypeToTemplate);
222        }
223
224}