001package ca.uhn.fhir.jpa.term.custom;
002
003/*-
004 * #%L
005 * HAPI FHIR JPA Server
006 * %%
007 * Copyright (C) 2014 - 2021 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.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 org.apache.commons.csv.QuoteMode;
031import org.apache.commons.lang3.Validate;
032
033import javax.annotation.Nonnull;
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 void addRootConcept(String theCode) {
065                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(myRootConcepts.stream().noneMatch(t -> t.getCode().equals(theCode)), "Already have code %s", theCode);
071                TermConcept retVal = new TermConcept();
072                retVal.setCode(theCode);
073                retVal.setDisplay(theDisplay);
074                myRootConcepts.add(retVal);
075                return retVal;
076        }
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("Cycle detected around code " + next.getCode());
120                }
121                validateNoCycleOrThrowInvalidRequest(theCodes, next.getChildCodes());
122        }
123
124        public Set<String> getRootConceptCodes() {
125                return getRootConcepts()
126                        .stream()
127                        .map(TermConcept::getCode)
128                        .collect(Collectors.toSet());
129        }
130
131        @Nonnull
132        public static CustomTerminologySet load(LoadedFileDescriptors theDescriptors, boolean theFlat) {
133
134                final Map<String, TermConcept> code2concept = new LinkedHashMap<>();
135                // Concepts
136                IZipContentsHandlerCsv conceptHandler = new ConceptHandler(code2concept);
137
138                TermLoaderSvcImpl.iterateOverZipFileCsv(theDescriptors, TermLoaderSvcImpl.CUSTOM_CONCEPTS_FILE, conceptHandler, ',', QuoteMode.NON_NUMERIC, false);
139
140                if (theDescriptors.hasFile(TermLoaderSvcImpl.CUSTOM_PROPERTIES_FILE)) {
141                        Map<String, List<TermConceptProperty>> theCode2property = new LinkedHashMap<>();
142                        IZipContentsHandlerCsv propertyHandler = new PropertyHandler(theCode2property);
143                        TermLoaderSvcImpl.iterateOverZipFileCsv(theDescriptors, TermLoaderSvcImpl.CUSTOM_PROPERTIES_FILE, propertyHandler, ',', QuoteMode.NON_NUMERIC, false);
144                        for (TermConcept termConcept : code2concept.values()) {
145                                if (!theCode2property.isEmpty() &&  theCode2property.get(termConcept.getCode()) != null) {
146                                        theCode2property.get(termConcept.getCode()).forEach(property -> {
147                                                termConcept.getProperties().add(property);
148                                        });
149                                }
150                        }
151                }
152
153                if (theFlat) {
154
155                        return new CustomTerminologySet(code2concept.size(), new ArrayList<>(code2concept.values()));
156
157                } else {
158
159                        // Hierarchy
160                        if (theDescriptors.hasFile(TermLoaderSvcImpl.CUSTOM_HIERARCHY_FILE)) {
161                                IZipContentsHandlerCsv hierarchyHandler = new HierarchyHandler(code2concept);
162                                TermLoaderSvcImpl.iterateOverZipFileCsv(theDescriptors, TermLoaderSvcImpl.CUSTOM_HIERARCHY_FILE, hierarchyHandler, ',', QuoteMode.NON_NUMERIC, false);
163                        }
164
165                        Map<String, Integer> codesInOrder = new HashMap<>();
166                        for (String nextCode : code2concept.keySet()) {
167                                codesInOrder.put(nextCode, codesInOrder.size());
168                        }
169
170                        List<TermConcept> rootConcepts = new ArrayList<>();
171                        for (TermConcept nextConcept : code2concept.values()) {
172
173                                // Find root concepts
174                                if (nextConcept.getParents().isEmpty()) {
175                                        rootConcepts.add(nextConcept);
176                                }
177
178                                // Sort children so they appear in the same order as they did in the concepts.csv file
179                                nextConcept.getChildren().sort((o1, o2) -> {
180                                        String code1 = o1.getChild().getCode();
181                                        String code2 = o2.getChild().getCode();
182                                        int order1 = codesInOrder.get(code1);
183                                        int order2 = codesInOrder.get(code2);
184                                        return order1 - order2;
185                                });
186
187                        }
188
189                        return new CustomTerminologySet(code2concept.size(), rootConcepts);
190                }
191        }
192
193}