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.term.loinc;
021
022import ca.uhn.fhir.jpa.entity.TermConcept;
023import ca.uhn.fhir.jpa.term.IZipContentsHandlerCsv;
024import ca.uhn.fhir.jpa.term.api.ITermLoaderSvc;
025import org.apache.commons.csv.CSVRecord;
026import org.hl7.fhir.r4.model.CodeSystem;
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029
030import java.util.Arrays;
031import java.util.List;
032import java.util.Map;
033import java.util.stream.Collectors;
034
035import static org.apache.commons.lang3.StringUtils.isBlank;
036import static org.apache.commons.lang3.StringUtils.isNotBlank;
037import static org.apache.commons.lang3.StringUtils.trim;
038
039/**
040 * Handler to process coding type properties 'AskAtOrderEntry' and 'AssociatedObservations'.
041 *
042 * These properties are added in a specific handler which is involved after all TermConcepts
043 * are created, because they require a 'display' value associated to other TermConcept (pointed by the 'code'
044 * property value), which require that concept to have been created.
045 */
046public class LoincCodingPropertiesHandler implements IZipContentsHandlerCsv {
047        private static final Logger ourLog = LoggerFactory.getLogger(LoincCodingPropertiesHandler.class);
048
049        public static final String ASK_AT_ORDER_ENTRY_PROP_NAME = "AskAtOrderEntry";
050        public static final String ASSOCIATED_OBSERVATIONS_PROP_NAME = "AssociatedObservations";
051        public static final String LOINC_NUM = "LOINC_NUM";
052
053        private final Map<String, TermConcept> myCode2Concept;
054        private final Map<String, CodeSystem.PropertyType> myPropertyNameTypeMap;
055
056        public LoincCodingPropertiesHandler(
057                        Map<String, TermConcept> theCode2concept, Map<String, CodeSystem.PropertyType> thePropertyNameTypeMap) {
058                myCode2Concept = theCode2concept;
059                myPropertyNameTypeMap = thePropertyNameTypeMap;
060        }
061
062        @Override
063        public void accept(CSVRecord theRecord) {
064                if (!anyValidProperty()) {
065                        return;
066                }
067
068                String code = trim(theRecord.get(LOINC_NUM));
069                if (isBlank(code)) {
070                        return;
071                }
072
073                String askAtOrderEntryValue = trim(theRecord.get(ASK_AT_ORDER_ENTRY_PROP_NAME));
074                String associatedObservationsValue = trim(theRecord.get(ASSOCIATED_OBSERVATIONS_PROP_NAME));
075
076                // any of the record properties have a valid value?
077                if (isBlank(askAtOrderEntryValue) && isBlank(associatedObservationsValue)) {
078                        return;
079                }
080
081                TermConcept srcTermConcept = myCode2Concept.get(code);
082
083                if (isNotBlank(askAtOrderEntryValue)) {
084                        addCodingProperties(srcTermConcept, ASK_AT_ORDER_ENTRY_PROP_NAME, askAtOrderEntryValue);
085                }
086
087                if (isNotBlank(associatedObservationsValue)) {
088                        addCodingProperties(srcTermConcept, ASSOCIATED_OBSERVATIONS_PROP_NAME, associatedObservationsValue);
089                }
090        }
091
092        /**
093         * Validates that at least one ot target properties is defined in loinc.xml file and is of type "CODING"
094         */
095        private boolean anyValidProperty() {
096                CodeSystem.PropertyType askAtOrderEntryPropType = myPropertyNameTypeMap.get(ASK_AT_ORDER_ENTRY_PROP_NAME);
097                CodeSystem.PropertyType associatedObservationsPropType =
098                                myPropertyNameTypeMap.get(ASSOCIATED_OBSERVATIONS_PROP_NAME);
099
100                return askAtOrderEntryPropType == CodeSystem.PropertyType.CODING
101                                || associatedObservationsPropType == CodeSystem.PropertyType.CODING;
102        }
103
104        private void addCodingProperties(TermConcept theSrcTermConcept, String thePropertyName, String thePropertyValue) {
105                List<String> propertyCodeValues = parsePropertyCodeValues(thePropertyValue);
106                for (String propertyCodeValue : propertyCodeValues) {
107                        TermConcept targetTermConcept = myCode2Concept.get(propertyCodeValue);
108                        if (targetTermConcept == null) {
109                                ourLog.error(
110                                                "Couldn't find TermConcept for code: '{}'. Display property set to blank for property: '{}'",
111                                                propertyCodeValue,
112                                                thePropertyName);
113                                continue;
114                        }
115                        theSrcTermConcept.addPropertyCoding(
116                                        thePropertyName, ITermLoaderSvc.LOINC_URI, propertyCodeValue, targetTermConcept.getDisplay());
117                        ourLog.trace("Adding coding property: {} to concept.code {}", thePropertyName, theSrcTermConcept.getCode());
118                }
119        }
120
121        private List<String> parsePropertyCodeValues(String theValue) {
122                return Arrays.stream(theValue.split(";")).map(String::trim).collect(Collectors.toList());
123        }
124}