001/*
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2024 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.jpa.provider;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.context.RuntimeResourceDefinition;
024import ca.uhn.fhir.i18n.Msg;
025import ca.uhn.fhir.jpa.model.util.JpaConstants;
026import ca.uhn.fhir.jpa.term.TermLoaderSvcImpl;
027import ca.uhn.fhir.jpa.term.UploadStatistics;
028import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc;
029import ca.uhn.fhir.jpa.term.custom.ConceptHandler;
030import ca.uhn.fhir.jpa.term.custom.HierarchyHandler;
031import ca.uhn.fhir.jpa.term.custom.PropertyHandler;
032import ca.uhn.fhir.rest.annotation.Operation;
033import ca.uhn.fhir.rest.annotation.OperationParam;
034import ca.uhn.fhir.rest.api.server.RequestDetails;
035import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
036import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
037import ca.uhn.fhir.util.AttachmentUtil;
038import ca.uhn.fhir.util.ParametersUtil;
039import ca.uhn.fhir.util.ValidateUtil;
040import com.google.common.base.Charsets;
041import com.google.common.collect.ArrayListMultimap;
042import com.google.common.collect.Multimap;
043import jakarta.annotation.Nonnull;
044import jakarta.servlet.http.HttpServletRequest;
045import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_30_40;
046import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_40_50;
047import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_40;
048import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50;
049import org.hl7.fhir.instance.model.api.IBaseParameters;
050import org.hl7.fhir.instance.model.api.IBaseResource;
051import org.hl7.fhir.instance.model.api.ICompositeType;
052import org.hl7.fhir.instance.model.api.IPrimitiveType;
053import org.hl7.fhir.r4.model.CodeSystem;
054import org.springframework.beans.factory.annotation.Autowired;
055
056import java.io.File;
057import java.io.FileInputStream;
058import java.io.FileNotFoundException;
059import java.io.InputStream;
060import java.util.ArrayList;
061import java.util.LinkedHashMap;
062import java.util.List;
063import java.util.Map;
064
065import static org.apache.commons.lang3.StringUtils.isBlank;
066import static org.apache.commons.lang3.StringUtils.isNotBlank;
067import static org.apache.commons.lang3.StringUtils.trim;
068
069public class TerminologyUploaderProvider extends BaseJpaProvider {
070
071        public static final String PARAM_FILE = "file";
072        public static final String PARAM_CODESYSTEM = "codeSystem";
073        public static final String PARAM_SYSTEM = "system";
074        private static final String RESP_PARAM_CONCEPT_COUNT = "conceptCount";
075        private static final String RESP_PARAM_TARGET = "target";
076        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TerminologyUploaderProvider.class);
077        private static final String RESP_PARAM_SUCCESS = "success";
078
079        @Autowired
080        private ITermLoaderSvc myTerminologyLoaderSvc;
081
082        /**
083         * Constructor
084         */
085        public TerminologyUploaderProvider() {
086                this(null, null);
087        }
088
089        /**
090         * Constructor
091         */
092        public TerminologyUploaderProvider(FhirContext theContext, ITermLoaderSvc theTerminologyLoaderSvc) {
093                setContext(theContext);
094                myTerminologyLoaderSvc = theTerminologyLoaderSvc;
095        }
096
097        /**
098         * <code>
099         * $upload-external-codesystem
100         * </code>
101         */
102        @Operation(
103                        typeName = "CodeSystem",
104                        name = JpaConstants.OPERATION_UPLOAD_EXTERNAL_CODE_SYSTEM,
105                        idempotent = false,
106                        returnParameters = {
107                                //              @OperationParam(name = "conceptCount", type = IntegerType.class, min = 1)
108                        })
109        public IBaseParameters uploadSnapshot(
110                        HttpServletRequest theServletRequest,
111                        @OperationParam(name = PARAM_SYSTEM, min = 1, typeName = "uri") IPrimitiveType<String> theCodeSystemUrl,
112                        @OperationParam(name = PARAM_FILE, min = 0, max = OperationParam.MAX_UNLIMITED, typeName = "attachment")
113                                        List<ICompositeType> theFiles,
114                        RequestDetails theRequestDetails) {
115
116                startRequest(theServletRequest);
117
118                if (theCodeSystemUrl == null || isBlank(theCodeSystemUrl.getValueAsString())) {
119                        throw new InvalidRequestException(Msg.code(1137) + "Missing mandatory parameter: " + PARAM_SYSTEM);
120                }
121
122                if (theFiles == null || theFiles.size() == 0) {
123                        throw new InvalidRequestException(
124                                        Msg.code(1138) + "No '" + PARAM_FILE + "' parameter, or package had no data");
125                }
126                for (ICompositeType next : theFiles) {
127                        ValidateUtil.isTrueOrThrowInvalidRequest(
128                                        getContext().getElementDefinition(next.getClass()).getName().equals("Attachment"),
129                                        "Package must be of type Attachment");
130                }
131
132                try {
133                        List<ITermLoaderSvc.FileDescriptor> localFiles = convertAttachmentsToFileDescriptors(theFiles);
134
135                        String codeSystemUrl = theCodeSystemUrl.getValue();
136                        codeSystemUrl = trim(codeSystemUrl);
137
138                        UploadStatistics stats;
139                        switch (codeSystemUrl) {
140                                case ITermLoaderSvc.ICD10_URI:
141                                        stats = myTerminologyLoaderSvc.loadIcd10(localFiles, theRequestDetails);
142                                        break;
143                                case ITermLoaderSvc.ICD10CM_URI:
144                                        stats = myTerminologyLoaderSvc.loadIcd10cm(localFiles, theRequestDetails);
145                                        break;
146                                case ITermLoaderSvc.IMGTHLA_URI:
147                                        stats = myTerminologyLoaderSvc.loadImgthla(localFiles, theRequestDetails);
148                                        break;
149                                case ITermLoaderSvc.LOINC_URI:
150                                        stats = myTerminologyLoaderSvc.loadLoinc(localFiles, theRequestDetails);
151                                        break;
152                                case ITermLoaderSvc.SCT_URI:
153                                        stats = myTerminologyLoaderSvc.loadSnomedCt(localFiles, theRequestDetails);
154                                        break;
155                                default:
156                                        stats = myTerminologyLoaderSvc.loadCustom(codeSystemUrl, localFiles, theRequestDetails);
157                                        break;
158                        }
159
160                        IBaseParameters retVal = ParametersUtil.newInstance(getContext());
161                        ParametersUtil.addParameterToParametersBoolean(getContext(), retVal, RESP_PARAM_SUCCESS, true);
162                        ParametersUtil.addParameterToParametersInteger(
163                                        getContext(), retVal, RESP_PARAM_CONCEPT_COUNT, stats.getUpdatedConceptCount());
164                        ParametersUtil.addParameterToParametersReference(
165                                        getContext(), retVal, RESP_PARAM_TARGET, stats.getTarget().getValue());
166
167                        return retVal;
168                } finally {
169                        endRequest(theServletRequest);
170                }
171        }
172
173        /**
174         * <code>
175         * $apply-codesystem-delta-add
176         * </code>
177         */
178        @Operation(
179                        typeName = "CodeSystem",
180                        name = JpaConstants.OPERATION_APPLY_CODESYSTEM_DELTA_ADD,
181                        idempotent = false,
182                        returnParameters = {})
183        public IBaseParameters uploadDeltaAdd(
184                        HttpServletRequest theServletRequest,
185                        @OperationParam(name = PARAM_SYSTEM, min = 1, max = 1, typeName = "uri") IPrimitiveType<String> theSystem,
186                        @OperationParam(name = PARAM_FILE, min = 0, max = OperationParam.MAX_UNLIMITED, typeName = "attachment")
187                                        List<ICompositeType> theFiles,
188                        @OperationParam(
189                                                        name = PARAM_CODESYSTEM,
190                                                        min = 0,
191                                                        max = OperationParam.MAX_UNLIMITED,
192                                                        typeName = "CodeSystem")
193                                        List<IBaseResource> theCodeSystems,
194                        RequestDetails theRequestDetails) {
195
196                startRequest(theServletRequest);
197                try {
198                        validateHaveSystem(theSystem);
199                        validateHaveFiles(theFiles, theCodeSystems);
200
201                        List<ITermLoaderSvc.FileDescriptor> files = convertAttachmentsToFileDescriptors(theFiles);
202                        convertCodeSystemsToFileDescriptors(files, theCodeSystems);
203                        UploadStatistics outcome =
204                                        myTerminologyLoaderSvc.loadDeltaAdd(theSystem.getValue(), files, theRequestDetails);
205                        return toDeltaResponse(outcome);
206                } finally {
207                        endRequest(theServletRequest);
208                }
209        }
210
211        /**
212         * <code>
213         * $apply-codesystem-delta-remove
214         * </code>
215         */
216        @Operation(
217                        typeName = "CodeSystem",
218                        name = JpaConstants.OPERATION_APPLY_CODESYSTEM_DELTA_REMOVE,
219                        idempotent = false,
220                        returnParameters = {})
221        public IBaseParameters uploadDeltaRemove(
222                        HttpServletRequest theServletRequest,
223                        @OperationParam(name = PARAM_SYSTEM, min = 1, max = 1, typeName = "uri") IPrimitiveType<String> theSystem,
224                        @OperationParam(name = PARAM_FILE, min = 0, max = OperationParam.MAX_UNLIMITED, typeName = "attachment")
225                                        List<ICompositeType> theFiles,
226                        @OperationParam(
227                                                        name = PARAM_CODESYSTEM,
228                                                        min = 0,
229                                                        max = OperationParam.MAX_UNLIMITED,
230                                                        typeName = "CodeSystem")
231                                        List<IBaseResource> theCodeSystems,
232                        RequestDetails theRequestDetails) {
233
234                startRequest(theServletRequest);
235                try {
236                        validateHaveSystem(theSystem);
237                        validateHaveFiles(theFiles, theCodeSystems);
238
239                        List<ITermLoaderSvc.FileDescriptor> files = convertAttachmentsToFileDescriptors(theFiles);
240                        convertCodeSystemsToFileDescriptors(files, theCodeSystems);
241                        UploadStatistics outcome =
242                                        myTerminologyLoaderSvc.loadDeltaRemove(theSystem.getValue(), files, theRequestDetails);
243                        return toDeltaResponse(outcome);
244                } finally {
245                        endRequest(theServletRequest);
246                }
247        }
248
249        private void convertCodeSystemsToFileDescriptors(
250                        List<ITermLoaderSvc.FileDescriptor> theFiles, List<IBaseResource> theCodeSystems) {
251                Map<String, String> codes = new LinkedHashMap<>();
252                Map<String, List<CodeSystem.ConceptPropertyComponent>> codeToProperties = new LinkedHashMap<>();
253
254                Multimap<String, String> codeToParentCodes = ArrayListMultimap.create();
255
256                if (theCodeSystems != null) {
257                        for (IBaseResource nextCodeSystemUncast : theCodeSystems) {
258                                CodeSystem nextCodeSystem = canonicalizeCodeSystem(nextCodeSystemUncast);
259                                convertCodeSystemCodesToCsv(
260                                                nextCodeSystem.getConcept(), codes, codeToProperties, null, codeToParentCodes);
261                        }
262                }
263
264                // Create concept file
265                if (codes.size() > 0) {
266                        StringBuilder b = new StringBuilder();
267                        b.append(ConceptHandler.CODE);
268                        b.append(",");
269                        b.append(ConceptHandler.DISPLAY);
270                        b.append("\n");
271                        for (Map.Entry<String, String> nextEntry : codes.entrySet()) {
272                                b.append(csvEscape(nextEntry.getKey()));
273                                b.append(",");
274                                b.append(csvEscape(nextEntry.getValue()));
275                                b.append("\n");
276                        }
277                        byte[] bytes = b.toString().getBytes(Charsets.UTF_8);
278                        String fileName = TermLoaderSvcImpl.CUSTOM_CONCEPTS_FILE;
279                        ITermLoaderSvc.ByteArrayFileDescriptor fileDescriptor =
280                                        new ITermLoaderSvc.ByteArrayFileDescriptor(fileName, bytes);
281                        theFiles.add(fileDescriptor);
282                }
283
284                // Create hierarchy file
285                if (codeToParentCodes.size() > 0) {
286                        StringBuilder b = new StringBuilder();
287                        b.append(HierarchyHandler.CHILD);
288                        b.append(",");
289                        b.append(HierarchyHandler.PARENT);
290                        b.append("\n");
291                        for (Map.Entry<String, String> nextEntry : codeToParentCodes.entries()) {
292                                b.append(csvEscape(nextEntry.getKey()));
293                                b.append(",");
294                                b.append(csvEscape(nextEntry.getValue()));
295                                b.append("\n");
296                        }
297                        byte[] bytes = b.toString().getBytes(Charsets.UTF_8);
298                        String fileName = TermLoaderSvcImpl.CUSTOM_HIERARCHY_FILE;
299                        ITermLoaderSvc.ByteArrayFileDescriptor fileDescriptor =
300                                        new ITermLoaderSvc.ByteArrayFileDescriptor(fileName, bytes);
301                        theFiles.add(fileDescriptor);
302                }
303                // Create codeToProperties file
304                if (codeToProperties.size() > 0) {
305                        StringBuilder b = new StringBuilder();
306                        b.append(PropertyHandler.CODE);
307                        b.append(",");
308                        b.append(PropertyHandler.KEY);
309                        b.append(",");
310                        b.append(PropertyHandler.VALUE);
311                        b.append(",");
312                        b.append(PropertyHandler.TYPE);
313                        b.append("\n");
314
315                        for (Map.Entry<String, List<CodeSystem.ConceptPropertyComponent>> nextEntry : codeToProperties.entrySet()) {
316                                for (CodeSystem.ConceptPropertyComponent propertyComponent : nextEntry.getValue()) {
317                                        b.append(csvEscape(nextEntry.getKey()));
318                                        b.append(",");
319                                        b.append(csvEscape(propertyComponent.getCode()));
320                                        b.append(",");
321                                        // TODO: check this for different types, other types should be added once
322                                        // TermConceptPropertyTypeEnum contain different types
323                                        b.append(csvEscape(propertyComponent.getValueStringType().getValue()));
324                                        b.append(",");
325                                        b.append(csvEscape(propertyComponent.getValue().primitiveValue()));
326                                        b.append("\n");
327                                }
328                        }
329                        byte[] bytes = b.toString().getBytes(Charsets.UTF_8);
330                        String fileName = TermLoaderSvcImpl.CUSTOM_PROPERTIES_FILE;
331                        ITermLoaderSvc.ByteArrayFileDescriptor fileDescriptor =
332                                        new ITermLoaderSvc.ByteArrayFileDescriptor(fileName, bytes);
333                        theFiles.add(fileDescriptor);
334                }
335        }
336
337        @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
338        @Nonnull
339        CodeSystem canonicalizeCodeSystem(@Nonnull IBaseResource theCodeSystem) {
340                RuntimeResourceDefinition resourceDef = getContext().getResourceDefinition(theCodeSystem);
341                ValidateUtil.isTrueOrThrowInvalidRequest(
342                                resourceDef.getName().equals("CodeSystem"), "Resource '%s' is not a CodeSystem", resourceDef.getName());
343
344                CodeSystem nextCodeSystem;
345                switch (getContext().getVersion().getVersion()) {
346                        case DSTU3:
347                                nextCodeSystem = (CodeSystem) VersionConvertorFactory_30_40.convertResource(
348                                                (org.hl7.fhir.dstu3.model.CodeSystem) theCodeSystem, new BaseAdvisor_30_40(false));
349                                break;
350                        case R5:
351                                nextCodeSystem = (CodeSystem) VersionConvertorFactory_40_50.convertResource(
352                                                (org.hl7.fhir.r5.model.CodeSystem) theCodeSystem, new BaseAdvisor_40_50(false));
353                                break;
354                        default:
355                                nextCodeSystem = (CodeSystem) theCodeSystem;
356                }
357                return nextCodeSystem;
358        }
359
360        private void convertCodeSystemCodesToCsv(
361                        List<CodeSystem.ConceptDefinitionComponent> theConcept,
362                        Map<String, String> theCodes,
363                        Map<String, List<CodeSystem.ConceptPropertyComponent>> theProperties,
364                        String theParentCode,
365                        Multimap<String, String> theCodeToParentCodes) {
366                for (CodeSystem.ConceptDefinitionComponent nextConcept : theConcept) {
367                        if (isNotBlank(nextConcept.getCode())) {
368                                theCodes.put(nextConcept.getCode(), nextConcept.getDisplay());
369                                if (isNotBlank(theParentCode)) {
370                                        theCodeToParentCodes.put(nextConcept.getCode(), theParentCode);
371                                }
372                                if (nextConcept.getProperty() != null) {
373                                        theProperties.put(nextConcept.getCode(), nextConcept.getProperty());
374                                }
375                                convertCodeSystemCodesToCsv(
376                                                nextConcept.getConcept(), theCodes, theProperties, nextConcept.getCode(), theCodeToParentCodes);
377                        }
378                }
379        }
380
381        private void validateHaveSystem(IPrimitiveType<String> theSystem) {
382                if (theSystem == null || isBlank(theSystem.getValueAsString())) {
383                        throw new InvalidRequestException(Msg.code(1139) + "Missing mandatory parameter: " + PARAM_SYSTEM);
384                }
385        }
386
387        private void validateHaveFiles(List<ICompositeType> theFiles, List<IBaseResource> theCodeSystems) {
388                if (theFiles != null) {
389                        for (ICompositeType nextFile : theFiles) {
390                                if (!nextFile.isEmpty()) {
391                                        return;
392                                }
393                        }
394                }
395                if (theCodeSystems != null) {
396                        for (IBaseResource next : theCodeSystems) {
397                                if (!next.isEmpty()) {
398                                        return;
399                                }
400                        }
401                }
402                throw new InvalidRequestException(Msg.code(1140) + "Missing mandatory parameter: " + PARAM_FILE);
403        }
404
405        @Nonnull
406        private List<ITermLoaderSvc.FileDescriptor> convertAttachmentsToFileDescriptors(
407                        @OperationParam(name = PARAM_FILE, min = 0, max = OperationParam.MAX_UNLIMITED, typeName = "attachment")
408                                        List<ICompositeType> theFiles) {
409                List<ITermLoaderSvc.FileDescriptor> files = new ArrayList<>();
410                if (theFiles != null) {
411                        for (ICompositeType next : theFiles) {
412
413                                String nextUrl =
414                                                AttachmentUtil.getOrCreateUrl(getContext(), next).getValue();
415                                ValidateUtil.isNotBlankOrThrowUnprocessableEntity(nextUrl, "Missing Attachment.url value");
416
417                                byte[] nextData;
418                                if (nextUrl.startsWith("localfile:")) {
419                                        String nextLocalFile = nextUrl.substring("localfile:".length());
420
421                                        if (isNotBlank(nextLocalFile)) {
422                                                ourLog.info("Reading in local file: {}", nextLocalFile);
423                                                File nextFile = new File(nextLocalFile);
424                                                if (!nextFile.exists() || !nextFile.isFile()) {
425                                                        throw new InvalidRequestException(Msg.code(1141) + "Unknown file: " + nextFile.getName());
426                                                }
427                                                files.add(new FileBackedFileDescriptor(nextFile));
428                                        }
429
430                                } else {
431                                        nextData =
432                                                        AttachmentUtil.getOrCreateData(getContext(), next).getValue();
433                                        ValidateUtil.isTrueOrThrowInvalidRequest(
434                                                        nextData != null && nextData.length > 0, "Missing Attachment.data value");
435                                        files.add(new ITermLoaderSvc.ByteArrayFileDescriptor(nextUrl, nextData));
436                                }
437                        }
438                }
439                return files;
440        }
441
442        private IBaseParameters toDeltaResponse(UploadStatistics theOutcome) {
443                IBaseParameters retVal = ParametersUtil.newInstance(getContext());
444                ParametersUtil.addParameterToParametersInteger(
445                                getContext(), retVal, RESP_PARAM_CONCEPT_COUNT, theOutcome.getUpdatedConceptCount());
446                ParametersUtil.addParameterToParametersReference(
447                                getContext(), retVal, RESP_PARAM_TARGET, theOutcome.getTarget().getValue());
448                return retVal;
449        }
450
451        public void setTerminologyLoaderSvc(ITermLoaderSvc theTermLoaderSvc) {
452                myTerminologyLoaderSvc = theTermLoaderSvc;
453        }
454
455        public static class FileBackedFileDescriptor implements ITermLoaderSvc.FileDescriptor {
456                private final File myNextFile;
457
458                public FileBackedFileDescriptor(File theNextFile) {
459                        myNextFile = theNextFile;
460                }
461
462                @Override
463                public String getFilename() {
464                        return myNextFile.getAbsolutePath();
465                }
466
467                @Override
468                public InputStream getInputStream() {
469                        try {
470                                return new FileInputStream(myNextFile);
471                        } catch (FileNotFoundException theE) {
472                                throw new InternalErrorException(Msg.code(1142) + theE);
473                        }
474                }
475        }
476
477        private static String csvEscape(String theValue) {
478                if (theValue == null) {
479                        return "";
480                }
481                return '"' + theValue.replace("\"", "\"\"").replace("\n", "\\n").replace("\r", "") + '"';
482        }
483}