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}