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