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; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.jpa.entity.TermConceptDesignation; 025import ca.uhn.fhir.jpa.term.ex.ExpansionTooCostlyException; 026import ca.uhn.fhir.model.api.annotation.Block; 027import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 028import jakarta.annotation.Nonnull; 029import jakarta.annotation.Nullable; 030import org.apache.commons.lang3.StringUtils; 031import org.hl7.fhir.r4.model.ValueSet; 032 033import java.util.ArrayList; 034import java.util.Arrays; 035import java.util.Collection; 036import java.util.Collections; 037import java.util.HashMap; 038import java.util.List; 039import java.util.Map; 040import java.util.stream.Collectors; 041 042import static org.apache.commons.lang3.StringUtils.isNotBlank; 043 044@Block() 045public class ValueSetExpansionComponentWithConceptAccumulator extends ValueSet.ValueSetExpansionComponent 046 implements IValueSetConceptAccumulator { 047 private final int myMaxCapacity; 048 private final FhirContext myContext; 049 private int mySkipCountRemaining; 050 private int myHardExpansionMaximumSize; 051 private List<String> myMessages; 052 private int myAddedConcepts; 053 private Integer myTotalConcepts; 054 private Map<Long, ValueSet.ValueSetExpansionContainsComponent> mySourcePidToConcept = new HashMap<>(); 055 private Map<ValueSet.ValueSetExpansionContainsComponent, String> myConceptToSourceDirectParentPids = 056 new HashMap<>(); 057 private boolean myTrackingHierarchy; 058 059 /** 060 * Constructor 061 * 062 * @param theMaxCapacity The maximum number of results this accumulator will accept before throwing 063 * an {@link InternalErrorException} 064 * @param theTrackingHierarchy 065 */ 066 ValueSetExpansionComponentWithConceptAccumulator( 067 FhirContext theContext, int theMaxCapacity, boolean theTrackingHierarchy) { 068 myMaxCapacity = theMaxCapacity; 069 myContext = theContext; 070 myTrackingHierarchy = theTrackingHierarchy; 071 } 072 073 @Nonnull 074 @Override 075 public Integer getCapacityRemaining() { 076 return (myMaxCapacity - myAddedConcepts) + mySkipCountRemaining; 077 } 078 079 public List<String> getMessages() { 080 if (myMessages == null) { 081 return Collections.emptyList(); 082 } 083 return Collections.unmodifiableList(myMessages); 084 } 085 086 @Override 087 public boolean isTrackingHierarchy() { 088 return myTrackingHierarchy; 089 } 090 091 @Override 092 public void addMessage(String theMessage) { 093 if (myMessages == null) { 094 myMessages = new ArrayList<>(); 095 } 096 myMessages.add(theMessage); 097 } 098 099 @Override 100 public void includeConcept( 101 String theSystem, 102 String theCode, 103 String theDisplay, 104 Long theSourceConceptPid, 105 String theSourceConceptDirectParentPids, 106 String theCodeSystemVersion) { 107 if (mySkipCountRemaining > 0) { 108 mySkipCountRemaining--; 109 return; 110 } 111 112 incrementConceptsCount(); 113 114 ValueSet.ValueSetExpansionContainsComponent contains = this.addContains(); 115 setSystemAndVersion(theSystem, contains); 116 contains.setCode(theCode); 117 contains.setDisplay(theDisplay); 118 contains.setVersion(theCodeSystemVersion); 119 } 120 121 @Override 122 public void includeConceptWithDesignations( 123 String theSystem, 124 String theCode, 125 String theDisplay, 126 Collection<TermConceptDesignation> theDesignations, 127 Long theSourceConceptPid, 128 String theSourceConceptDirectParentPids, 129 String theCodeSystemVersion) { 130 if (mySkipCountRemaining > 0) { 131 mySkipCountRemaining--; 132 return; 133 } 134 135 incrementConceptsCount(); 136 137 ValueSet.ValueSetExpansionContainsComponent contains = this.addContains(); 138 139 if (theSourceConceptPid != null) { 140 mySourcePidToConcept.put(theSourceConceptPid, contains); 141 } 142 if (theSourceConceptDirectParentPids != null) { 143 myConceptToSourceDirectParentPids.put(contains, theSourceConceptDirectParentPids); 144 } 145 146 setSystemAndVersion(theSystem, contains); 147 contains.setCode(theCode); 148 contains.setDisplay(theDisplay); 149 150 if (isNotBlank(theCodeSystemVersion)) { 151 contains.setVersion(theCodeSystemVersion); 152 } 153 154 if (theDesignations != null) { 155 for (TermConceptDesignation termConceptDesignation : theDesignations) { 156 contains.addDesignation() 157 .setValue(termConceptDesignation.getValue()) 158 .setLanguage(termConceptDesignation.getLanguage()) 159 .getUse() 160 .setSystem(termConceptDesignation.getUseSystem()) 161 .setCode(termConceptDesignation.getUseCode()) 162 .setDisplay(termConceptDesignation.getUseDisplay()); 163 } 164 } 165 } 166 167 @Override 168 public void consumeSkipCount(int theSkipCountToConsume) { 169 mySkipCountRemaining -= theSkipCountToConsume; 170 } 171 172 @Nullable 173 @Override 174 public Integer getSkipCountRemaining() { 175 return mySkipCountRemaining; 176 } 177 178 @Override 179 public boolean excludeConcept(String theSystem, String theCode) { 180 String excludeSystem; 181 String excludeSystemVersion; 182 int versionSeparator = theSystem.indexOf("|"); 183 if (versionSeparator > -1) { 184 excludeSystemVersion = theSystem.substring(versionSeparator + 1); 185 excludeSystem = theSystem.substring(0, versionSeparator); 186 } else { 187 excludeSystem = theSystem; 188 excludeSystemVersion = null; 189 } 190 if (excludeSystemVersion != null) { 191 return this.getContains() 192 .removeIf(t -> excludeSystem.equals(t.getSystem()) 193 && theCode.equals(t.getCode()) 194 && excludeSystemVersion.equals(t.getVersion())); 195 } else { 196 return this.getContains().removeIf(t -> theSystem.equals(t.getSystem()) && theCode.equals(t.getCode())); 197 } 198 } 199 200 private void incrementConceptsCount() { 201 Integer capacityRemaining = getCapacityRemaining(); 202 if (capacityRemaining == 0) { 203 String msg = myContext.getLocalizer().getMessage(TermReadSvcImpl.class, "expansionTooLarge", myMaxCapacity); 204 msg = appendAccumulatorMessages(msg); 205 throw new ExpansionTooCostlyException(Msg.code(831) + msg); 206 } 207 208 if (myHardExpansionMaximumSize > 0 && myAddedConcepts > myHardExpansionMaximumSize) { 209 String msg = myContext 210 .getLocalizer() 211 .getMessage(TermReadSvcImpl.class, "expansionTooLarge", myHardExpansionMaximumSize); 212 msg = appendAccumulatorMessages(msg); 213 throw new ExpansionTooCostlyException(Msg.code(832) + msg); 214 } 215 216 myAddedConcepts++; 217 } 218 219 @Nonnull 220 private String appendAccumulatorMessages(String msg) { 221 msg += getMessages().stream().map(t -> " - " + t).collect(Collectors.joining()); 222 return msg; 223 } 224 225 public Integer getTotalConcepts() { 226 return myTotalConcepts; 227 } 228 229 @Override 230 public void incrementOrDecrementTotalConcepts(boolean theAdd, int theDelta) { 231 int delta = theDelta; 232 if (!theAdd) { 233 delta = -delta; 234 } 235 if (myTotalConcepts == null) { 236 myTotalConcepts = delta; 237 } else { 238 myTotalConcepts = myTotalConcepts + delta; 239 } 240 } 241 242 private void setSystemAndVersion( 243 String theSystemAndVersion, ValueSet.ValueSetExpansionContainsComponent myComponent) { 244 if (StringUtils.isNotEmpty((theSystemAndVersion))) { 245 int versionSeparator = theSystemAndVersion.lastIndexOf('|'); 246 if (versionSeparator != -1) { 247 myComponent.setVersion(theSystemAndVersion.substring(versionSeparator + 1)); 248 myComponent.setSystem(theSystemAndVersion.substring(0, versionSeparator)); 249 } else { 250 myComponent.setSystem(theSystemAndVersion); 251 } 252 } 253 } 254 255 public void setSkipCountRemaining(int theSkipCountRemaining) { 256 mySkipCountRemaining = theSkipCountRemaining; 257 } 258 259 public void setHardExpansionMaximumSize(int theHardExpansionMaximumSize) { 260 myHardExpansionMaximumSize = theHardExpansionMaximumSize; 261 } 262 263 public void applyHierarchy() { 264 for (int i = 0; i < this.getContains().size(); i++) { 265 ValueSet.ValueSetExpansionContainsComponent nextContains = 266 this.getContains().get(i); 267 268 String directParentPidsString = myConceptToSourceDirectParentPids.get(nextContains); 269 if (isNotBlank(directParentPidsString) && !directParentPidsString.equals("NONE")) { 270 List<Long> directParentPids = Arrays.stream(directParentPidsString.split(" ")) 271 .map(t -> Long.parseLong(t)) 272 .collect(Collectors.toList()); 273 274 boolean firstMatch = false; 275 for (Long next : directParentPids) { 276 ValueSet.ValueSetExpansionContainsComponent parentConcept = mySourcePidToConcept.get(next); 277 if (parentConcept != null) { 278 if (!firstMatch) { 279 firstMatch = true; 280 this.getContains().remove(i); 281 i--; 282 } 283 284 parentConcept.addContains(nextContains); 285 } 286 } 287 } 288 } 289 } 290}