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