
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}