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