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}