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.jpa.dao.data.ITermValueSetConceptDao;
023import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDesignationDao;
024import ca.uhn.fhir.jpa.dao.data.ITermValueSetDao;
025import ca.uhn.fhir.jpa.entity.TermConceptDesignation;
026import ca.uhn.fhir.jpa.entity.TermValueSet;
027import ca.uhn.fhir.jpa.entity.TermValueSetConcept;
028import ca.uhn.fhir.jpa.entity.TermValueSetConceptDesignation;
029import ca.uhn.fhir.util.ValidateUtil;
030import jakarta.annotation.Nonnull;
031
032import java.util.Collection;
033import java.util.List;
034import java.util.Optional;
035
036import static org.apache.commons.lang3.StringUtils.isAnyBlank;
037import static org.apache.commons.lang3.StringUtils.isNoneBlank;
038import static org.apache.commons.lang3.StringUtils.isNotBlank;
039
040public class ValueSetConceptAccumulator implements IValueSetConceptAccumulator {
041        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ValueSetConceptAccumulator.class);
042
043        private TermValueSet myTermValueSet;
044        private final ITermValueSetDao myValueSetDao;
045        private final ITermValueSetConceptDao myValueSetConceptDao;
046        private final ITermValueSetConceptDesignationDao myValueSetConceptDesignationDao;
047        private int myConceptsSaved;
048        private int myDesignationsSaved;
049        private int myConceptsExcluded;
050
051        public ValueSetConceptAccumulator(
052                        @Nonnull TermValueSet theTermValueSet,
053                        @Nonnull ITermValueSetDao theValueSetDao,
054                        @Nonnull ITermValueSetConceptDao theValueSetConceptDao,
055                        @Nonnull ITermValueSetConceptDesignationDao theValueSetConceptDesignationDao) {
056                myTermValueSet = theTermValueSet;
057                myValueSetDao = theValueSetDao;
058                myValueSetConceptDao = theValueSetConceptDao;
059                myValueSetConceptDesignationDao = theValueSetConceptDesignationDao;
060                myConceptsSaved = 0;
061                myDesignationsSaved = 0;
062                myConceptsExcluded = 0;
063        }
064
065        @Override
066        public void addMessage(String theMessage) {
067                // ignore for now
068
069        }
070
071        @Override
072        public void includeConcept(
073                        String theSystem,
074                        String theCode,
075                        String theDisplay,
076                        Long theSourceConceptPid,
077                        String theSourceConceptDirectParentPids,
078                        String theSystemVersion) {
079                saveConcept(
080                                theSystem,
081                                theCode,
082                                theDisplay,
083                                theSourceConceptPid,
084                                theSourceConceptDirectParentPids,
085                                theSystemVersion);
086        }
087
088        @Override
089        public void includeConceptWithDesignations(
090                        String theSystem,
091                        String theCode,
092                        String theDisplay,
093                        Collection<TermConceptDesignation> theDesignations,
094                        Long theSourceConceptPid,
095                        String theSourceConceptDirectParentPids,
096                        String theSystemVersion) {
097                TermValueSetConcept concept = saveConcept(
098                                theSystem,
099                                theCode,
100                                theDisplay,
101                                theSourceConceptPid,
102                                theSourceConceptDirectParentPids,
103                                theSystemVersion);
104                if (theDesignations != null) {
105                        for (TermConceptDesignation designation : theDesignations) {
106                                saveConceptDesignation(concept, designation);
107                        }
108                }
109        }
110
111        @Override
112        public boolean excludeConcept(String theSystem, String theCode) {
113                if (isAnyBlank(theSystem, theCode)) {
114                        return false;
115                }
116
117                // Get existing entity so it can be deleted.
118                Optional<TermValueSetConcept> optionalConcept;
119                int versionIdx = theSystem.indexOf("|");
120                if (versionIdx >= 0) {
121                        String systemUrl = theSystem.substring(0, versionIdx);
122                        String systemVersion = theSystem.substring(versionIdx + 1);
123                        optionalConcept = myValueSetConceptDao.findByTermValueSetIdSystemAndCodeWithVersion(
124                                        myTermValueSet.getId(), systemUrl, systemVersion, theCode);
125                } else {
126                        optionalConcept =
127                                        myValueSetConceptDao.findByTermValueSetIdSystemAndCode(myTermValueSet.getId(), theSystem, theCode);
128                }
129
130                if (optionalConcept.isPresent()) {
131                        TermValueSetConcept concept = optionalConcept.get();
132
133                        ourLog.debug(
134                                        "Excluding [{}|{}] from ValueSet[{}]",
135                                        concept.getSystem(),
136                                        concept.getCode(),
137                                        myTermValueSet.getUrl());
138                        for (TermValueSetConceptDesignation designation : concept.getDesignations()) {
139                                myValueSetConceptDesignationDao.deleteById(designation.getId());
140                                myTermValueSet.decrementTotalConceptDesignations();
141                        }
142                        myValueSetConceptDao.deleteById(concept.getId());
143                        myTermValueSet.decrementTotalConcepts();
144                        myValueSetDao.save(myTermValueSet);
145                        ourLog.debug(
146                                        "Done excluding [{}|{}] from ValueSet[{}]",
147                                        concept.getSystem(),
148                                        concept.getCode(),
149                                        myTermValueSet.getUrl());
150
151                        if (++myConceptsExcluded % 250 == 0) {
152                                ourLog.info("Have excluded {} concepts from ValueSet[{}]", myConceptsExcluded, myTermValueSet.getUrl());
153                        }
154                }
155                return false;
156        }
157
158        private TermValueSetConcept saveConcept(
159                        String theSystem,
160                        String theCode,
161                        String theDisplay,
162                        Long theSourceConceptPid,
163                        String theSourceConceptDirectParentPids,
164                        String theSystemVersion) {
165                ValidateUtil.isNotBlankOrThrowInvalidRequest(theSystem, "ValueSet contains a concept with no system value");
166                ValidateUtil.isNotBlankOrThrowInvalidRequest(theCode, "ValueSet contains a concept with no code value");
167
168                TermValueSetConcept concept = new TermValueSetConcept();
169                concept.setValueSet(myTermValueSet);
170                concept.setOrder(myConceptsSaved);
171                int versionIndex = theSystem.indexOf("|");
172                if (versionIndex >= 0) {
173                        concept.setSystem(theSystem.substring(0, versionIndex));
174                        concept.setSystemVersion(theSystem.substring(versionIndex + 1));
175                } else {
176                        concept.setSystem(theSystem);
177                }
178                concept.setCode(theCode);
179                if (isNotBlank(theDisplay)) {
180                        concept.setDisplay(theDisplay);
181                }
182                concept.setSystemVersion(theSystemVersion);
183
184                concept.setSourceConceptPid(theSourceConceptPid);
185                concept.setSourceConceptDirectParentPids(theSourceConceptDirectParentPids);
186
187                myValueSetConceptDao.save(concept);
188                myValueSetDao.save(myTermValueSet.incrementTotalConcepts());
189
190                if (++myConceptsSaved % 250 == 0) {
191                        ourLog.info("Have pre-expanded {} concepts in ValueSet[{}]", myConceptsSaved, myTermValueSet.getUrl());
192                }
193
194                return concept;
195        }
196
197        private TermValueSetConceptDesignation saveConceptDesignation(
198                        TermValueSetConcept theConcept, TermConceptDesignation theDesignation) {
199                ValidateUtil.isNotBlankOrThrowInvalidRequest(
200                                theDesignation.getValue(), "ValueSet contains a concept designation with no value");
201
202                TermValueSetConceptDesignation designation = new TermValueSetConceptDesignation();
203                designation.setConcept(theConcept);
204                designation.setValueSet(myTermValueSet);
205                designation.setLanguage(theDesignation.getLanguage());
206                if (isNoneBlank(theDesignation.getUseSystem(), theDesignation.getUseCode())) {
207                        designation.setUseSystem(theDesignation.getUseSystem());
208                        designation.setUseCode(theDesignation.getUseCode());
209                        if (isNotBlank(theDesignation.getUseDisplay())) {
210                                designation.setUseDisplay(theDesignation.getUseDisplay());
211                        }
212                }
213                designation.setValue(theDesignation.getValue());
214                myValueSetConceptDesignationDao.save(designation);
215                myValueSetDao.save(myTermValueSet.incrementTotalConceptDesignations());
216
217                if (++myDesignationsSaved % 250 == 0) {
218                        ourLog.debug(
219                                        "Have pre-expanded {} designations for Concept[{}|{}] in ValueSet[{}]",
220                                        myDesignationsSaved,
221                                        theConcept.getSystem(),
222                                        theConcept.getCode(),
223                                        myTermValueSet.getUrl());
224                }
225
226                return designation;
227        }
228
229        public Boolean removeGapsFromConceptOrder() {
230                if (myConceptsExcluded <= 0) {
231                        return false;
232                }
233
234                ourLog.info("Removing gaps from concept order for ValueSet[{}]", myTermValueSet.getUrl());
235                int order = 0;
236                List<Long> conceptIds = myValueSetConceptDao.findIdsByTermValueSetId(myTermValueSet.getId());
237                for (Long conceptId : conceptIds) {
238                        myValueSetConceptDao.updateOrderById(conceptId, order++);
239                }
240                ourLog.info(
241                                "Have removed gaps from concept order for {} concepts in ValueSet[{}]",
242                                conceptIds.size(),
243                                myTermValueSet.getUrl());
244
245                return true;
246        }
247
248        public int getConceptsSaved() {
249                return myConceptsSaved;
250        }
251
252        // TODO: DM 2019-07-16 - We may need TermValueSetConceptProperty, similar to TermConceptProperty.
253        // TODO: DM 2019-07-16 - If so, we should also populate TermValueSetConceptProperty entities here.
254        // TODO: DM 2019-07-30 - Expansions don't include the properties themselves; they may be needed to facilitate
255        // filters and parameterized expansions.
256}