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.custom;
021
022import ca.uhn.fhir.i18n.Msg;
023import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
024import ca.uhn.fhir.jpa.entity.TermConcept;
025import ca.uhn.fhir.jpa.entity.TermConceptProperty;
026import ca.uhn.fhir.jpa.term.IZipContentsHandlerCsv;
027import ca.uhn.fhir.jpa.term.LoadedFileDescriptors;
028import ca.uhn.fhir.jpa.term.TermLoaderSvcImpl;
029import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
030import jakarta.annotation.Nonnull;
031import org.apache.commons.csv.QuoteMode;
032import org.apache.commons.lang3.Validate;
033
034import java.util.ArrayList;
035import java.util.Collections;
036import java.util.HashMap;
037import java.util.HashSet;
038import java.util.LinkedHashMap;
039import java.util.List;
040import java.util.Map;
041import java.util.Set;
042import java.util.stream.Collectors;
043
044public class CustomTerminologySet {
045
046        private final int mySize;
047        private final List<TermConcept> myRootConcepts;
048
049        /**
050         * Constructor for an empty object
051         */
052        public CustomTerminologySet() {
053                this(0, new ArrayList<>());
054        }
055
056        /**
057         * Constructor
058         */
059        private CustomTerminologySet(int theSize, List<TermConcept> theRootConcepts) {
060                mySize = theSize;
061                myRootConcepts = theRootConcepts;
062        }
063
064        public TermConcept addRootConcept(String theCode) {
065                return addRootConcept(theCode, null);
066        }
067
068        public TermConcept addRootConcept(String theCode, String theDisplay) {
069                Validate.notBlank(theCode, "theCode must not be blank");
070                Validate.isTrue(
071                                myRootConcepts.stream().noneMatch(t -> t.getCode().equals(theCode)), "Already have code %s", theCode);
072                TermConcept retVal = new TermConcept();
073                retVal.setCode(theCode);
074                retVal.setDisplay(theDisplay);
075                myRootConcepts.add(retVal);
076                return retVal;
077        }
078
079        public int getSize() {
080                return mySize;
081        }
082
083        public TermCodeSystemVersion toCodeSystemVersion() {
084                TermCodeSystemVersion csv = new TermCodeSystemVersion();
085
086                for (TermConcept next : myRootConcepts) {
087                        csv.getConcepts().add(next);
088                }
089
090                populateVersionToChildCodes(csv, myRootConcepts);
091
092                return csv;
093        }
094
095        private void populateVersionToChildCodes(TermCodeSystemVersion theCsv, List<TermConcept> theConcepts) {
096                for (TermConcept next : theConcepts) {
097                        next.setCodeSystemVersion(theCsv);
098                        populateVersionToChildCodes(theCsv, next.getChildCodes());
099                }
100        }
101
102        public List<TermConcept> getRootConcepts() {
103                return Collections.unmodifiableList(myRootConcepts);
104        }
105
106        public void validateNoCycleOrThrowInvalidRequest() {
107                Set<String> codes = new HashSet<>();
108                validateNoCycleOrThrowInvalidRequest(codes, getRootConcepts());
109        }
110
111        private void validateNoCycleOrThrowInvalidRequest(Set<String> theCodes, List<TermConcept> theRootConcepts) {
112                for (TermConcept next : theRootConcepts) {
113                        validateNoCycleOrThrowInvalidRequest(theCodes, next);
114                }
115        }
116
117        private void validateNoCycleOrThrowInvalidRequest(Set<String> theCodes, TermConcept next) {
118                if (!theCodes.add(next.getCode())) {
119                        throw new InvalidRequestException(Msg.code(926) + "Cycle detected around code " + next.getCode());
120                }
121                validateNoCycleOrThrowInvalidRequest(theCodes, next.getChildCodes());
122        }
123
124        public Set<String> getRootConceptCodes() {
125                return getRootConcepts().stream().map(TermConcept::getCode).collect(Collectors.toSet());
126        }
127
128        @Nonnull
129        public static CustomTerminologySet load(LoadedFileDescriptors theDescriptors, boolean theFlat) {
130
131                final Map<String, TermConcept> code2concept = new LinkedHashMap<>();
132                // Concepts
133                IZipContentsHandlerCsv conceptHandler = new ConceptHandler(code2concept);
134
135                TermLoaderSvcImpl.iterateOverZipFileCsv(
136                                theDescriptors,
137                                TermLoaderSvcImpl.CUSTOM_CONCEPTS_FILE,
138                                conceptHandler,
139                                ',',
140                                QuoteMode.NON_NUMERIC,
141                                false);
142
143                if (theDescriptors.hasFile(TermLoaderSvcImpl.CUSTOM_PROPERTIES_FILE)) {
144                        Map<String, List<TermConceptProperty>> theCode2property = new LinkedHashMap<>();
145                        IZipContentsHandlerCsv propertyHandler = new PropertyHandler(theCode2property);
146                        TermLoaderSvcImpl.iterateOverZipFileCsv(
147                                        theDescriptors,
148                                        TermLoaderSvcImpl.CUSTOM_PROPERTIES_FILE,
149                                        propertyHandler,
150                                        ',',
151                                        QuoteMode.NON_NUMERIC,
152                                        false);
153                        for (TermConcept termConcept : code2concept.values()) {
154                                if (!theCode2property.isEmpty() && theCode2property.get(termConcept.getCode()) != null) {
155                                        theCode2property.get(termConcept.getCode()).forEach(property -> {
156                                                termConcept.getProperties().add(property);
157                                        });
158                                }
159                        }
160                }
161
162                if (theFlat) {
163
164                        return new CustomTerminologySet(code2concept.size(), new ArrayList<>(code2concept.values()));
165
166                } else {
167
168                        // Hierarchy
169                        if (theDescriptors.hasFile(TermLoaderSvcImpl.CUSTOM_HIERARCHY_FILE)) {
170                                IZipContentsHandlerCsv hierarchyHandler = new HierarchyHandler(code2concept);
171                                TermLoaderSvcImpl.iterateOverZipFileCsv(
172                                                theDescriptors,
173                                                TermLoaderSvcImpl.CUSTOM_HIERARCHY_FILE,
174                                                hierarchyHandler,
175                                                ',',
176                                                QuoteMode.NON_NUMERIC,
177                                                false);
178                        }
179
180                        Map<String, Integer> codesInOrder = new HashMap<>();
181                        for (String nextCode : code2concept.keySet()) {
182                                codesInOrder.put(nextCode, codesInOrder.size());
183                        }
184
185                        List<TermConcept> rootConcepts = new ArrayList<>();
186                        for (TermConcept nextConcept : code2concept.values()) {
187
188                                // Find root concepts
189                                if (nextConcept.getParents().isEmpty()) {
190                                        rootConcepts.add(nextConcept);
191                                }
192
193                                // Sort children so they appear in the same order as they did in the concepts.csv file
194                                nextConcept.getChildren().sort((o1, o2) -> {
195                                        String code1 = o1.getChild().getCode();
196                                        String code2 = o2.getChild().getCode();
197                                        int order1 = codesInOrder.get(code1);
198                                        int order2 = codesInOrder.get(code2);
199                                        return order1 - order2;
200                                });
201                        }
202
203                        return new CustomTerminologySet(code2concept.size(), rootConcepts);
204                }
205        }
206}