001/*
002 * #%L
003 * HAPI FHIR JPA Server
004 * %%
005 * Copyright (C) 2014 - 2025 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.context.FhirVersionEnum;
024import ca.uhn.fhir.context.support.ConceptValidationOptions;
025import ca.uhn.fhir.context.support.IValidationSupport;
026import ca.uhn.fhir.context.support.LookupCodeRequest;
027import ca.uhn.fhir.context.support.ValidationSupportContext;
028import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
029import ca.uhn.fhir.i18n.Msg;
030import ca.uhn.fhir.interceptor.model.RequestPartitionId;
031import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
032import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
033import ca.uhn.fhir.jpa.api.dao.IDao;
034import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
035import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem;
036import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
037import ca.uhn.fhir.jpa.api.svc.ResolveIdentityMode;
038import ca.uhn.fhir.jpa.config.HibernatePropertiesProvider;
039import ca.uhn.fhir.jpa.config.util.ConnectionPoolInfoProvider;
040import ca.uhn.fhir.jpa.config.util.IConnectionPoolInfoProvider;
041import ca.uhn.fhir.jpa.dao.IFulltextSearchSvc;
042import ca.uhn.fhir.jpa.dao.IJpaStorageResourceParser;
043import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemDao;
044import ca.uhn.fhir.jpa.dao.data.ITermCodeSystemVersionDao;
045import ca.uhn.fhir.jpa.dao.data.ITermConceptDao;
046import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDao;
047import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptDesignationDao;
048import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptViewDao;
049import ca.uhn.fhir.jpa.dao.data.ITermValueSetConceptViewOracleDao;
050import ca.uhn.fhir.jpa.dao.data.ITermValueSetDao;
051import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
052import ca.uhn.fhir.jpa.entity.ITermValueSetConceptView;
053import ca.uhn.fhir.jpa.entity.TermCodeSystem;
054import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
055import ca.uhn.fhir.jpa.entity.TermConcept;
056import ca.uhn.fhir.jpa.entity.TermConceptDesignation;
057import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink;
058import ca.uhn.fhir.jpa.entity.TermConceptParentChildLink.RelationshipTypeEnum;
059import ca.uhn.fhir.jpa.entity.TermConceptProperty;
060import ca.uhn.fhir.jpa.entity.TermConceptPropertyTypeEnum;
061import ca.uhn.fhir.jpa.entity.TermValueSet;
062import ca.uhn.fhir.jpa.entity.TermValueSetConcept;
063import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum;
064import ca.uhn.fhir.jpa.model.config.PartitionSettings;
065import ca.uhn.fhir.jpa.model.dao.JpaPid;
066import ca.uhn.fhir.jpa.model.entity.ResourceTable;
067import ca.uhn.fhir.jpa.model.sched.HapiJob;
068import ca.uhn.fhir.jpa.model.sched.IHasScheduledJobs;
069import ca.uhn.fhir.jpa.model.sched.ISchedulerService;
070import ca.uhn.fhir.jpa.model.sched.ScheduledJobDefinition;
071import ca.uhn.fhir.jpa.model.util.JpaConstants;
072import ca.uhn.fhir.jpa.search.builder.SearchBuilder;
073import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
074import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
075import ca.uhn.fhir.jpa.term.api.ReindexTerminologyResult;
076import ca.uhn.fhir.jpa.term.ex.ExpansionTooCostlyException;
077import ca.uhn.fhir.rest.api.Constants;
078import ca.uhn.fhir.rest.api.server.RequestDetails;
079import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
080import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
081import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
082import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
083import ca.uhn.fhir.util.CoverageIgnore;
084import ca.uhn.fhir.util.FhirVersionIndependentConcept;
085import ca.uhn.fhir.util.HapiExtensions;
086import ca.uhn.fhir.util.StopWatch;
087import ca.uhn.fhir.util.UrlUtil;
088import ca.uhn.fhir.util.ValidateUtil;
089import ca.uhn.hapi.converters.canonical.VersionCanonicalizer;
090import com.google.common.annotations.VisibleForTesting;
091import com.google.common.base.Stopwatch;
092import com.google.common.collect.ArrayListMultimap;
093import jakarta.annotation.Nonnull;
094import jakarta.annotation.Nullable;
095import jakarta.annotation.PostConstruct;
096import jakarta.persistence.EntityManager;
097import jakarta.persistence.NonUniqueResultException;
098import jakarta.persistence.PersistenceContext;
099import jakarta.persistence.PersistenceContextType;
100import org.apache.commons.collections4.ListUtils;
101import org.apache.commons.lang3.ObjectUtils;
102import org.apache.commons.lang3.StringUtils;
103import org.apache.commons.lang3.Validate;
104import org.apache.commons.lang3.time.DateUtils;
105import org.apache.lucene.index.Term;
106import org.apache.lucene.search.IndexSearcher;
107import org.hibernate.CacheMode;
108import org.hibernate.search.engine.search.predicate.dsl.BooleanPredicateClausesStep;
109import org.hibernate.search.engine.search.predicate.dsl.PredicateFinalStep;
110import org.hibernate.search.engine.search.predicate.dsl.SearchPredicateFactory;
111import org.hibernate.search.engine.search.query.SearchQuery;
112import org.hibernate.search.engine.search.query.SearchScroll;
113import org.hibernate.search.engine.search.query.SearchScrollResult;
114import org.hibernate.search.mapper.orm.Search;
115import org.hibernate.search.mapper.orm.common.EntityReference;
116import org.hibernate.search.mapper.orm.session.SearchSession;
117import org.hibernate.search.mapper.pojo.massindexing.impl.PojoMassIndexingLoggingMonitor;
118import org.hl7.fhir.common.hapi.validation.support.InMemoryTerminologyServerValidationSupport;
119import org.hl7.fhir.convertors.advisors.impl.BaseAdvisor_40_50;
120import org.hl7.fhir.convertors.context.ConversionContext40_50;
121import org.hl7.fhir.convertors.conv40_50.VersionConvertor_40_50;
122import org.hl7.fhir.convertors.conv40_50.resources40_50.ValueSet40_50;
123import org.hl7.fhir.instance.model.api.IAnyResource;
124import org.hl7.fhir.instance.model.api.IBaseCoding;
125import org.hl7.fhir.instance.model.api.IBaseDatatype;
126import org.hl7.fhir.instance.model.api.IBaseResource;
127import org.hl7.fhir.instance.model.api.IIdType;
128import org.hl7.fhir.instance.model.api.IPrimitiveType;
129import org.hl7.fhir.r4.model.BooleanType;
130import org.hl7.fhir.r4.model.CanonicalType;
131import org.hl7.fhir.r4.model.CodeSystem;
132import org.hl7.fhir.r4.model.CodeableConcept;
133import org.hl7.fhir.r4.model.Coding;
134import org.hl7.fhir.r4.model.DateTimeType;
135import org.hl7.fhir.r4.model.DecimalType;
136import org.hl7.fhir.r4.model.DomainResource;
137import org.hl7.fhir.r4.model.Enumerations;
138import org.hl7.fhir.r4.model.Extension;
139import org.hl7.fhir.r4.model.InstantType;
140import org.hl7.fhir.r4.model.IntegerType;
141import org.hl7.fhir.r4.model.StringType;
142import org.hl7.fhir.r4.model.ValueSet;
143import org.hl7.fhir.r4.model.codesystems.ConceptSubsumptionOutcome;
144import org.quartz.JobExecutionContext;
145import org.springframework.beans.factory.annotation.Autowired;
146import org.springframework.context.ApplicationContext;
147import org.springframework.data.domain.PageRequest;
148import org.springframework.data.domain.Pageable;
149import org.springframework.data.domain.Slice;
150import org.springframework.transaction.PlatformTransactionManager;
151import org.springframework.transaction.TransactionDefinition;
152import org.springframework.transaction.annotation.Propagation;
153import org.springframework.transaction.annotation.Transactional;
154import org.springframework.transaction.interceptor.NoRollbackRuleAttribute;
155import org.springframework.transaction.interceptor.RuleBasedTransactionAttribute;
156import org.springframework.transaction.support.TransactionSynchronizationManager;
157import org.springframework.transaction.support.TransactionTemplate;
158import org.springframework.util.comparator.Comparators;
159
160import java.util.ArrayList;
161import java.util.Arrays;
162import java.util.Collection;
163import java.util.Collections;
164import java.util.Date;
165import java.util.HashMap;
166import java.util.HashSet;
167import java.util.LinkedHashMap;
168import java.util.List;
169import java.util.Map;
170import java.util.Objects;
171import java.util.Optional;
172import java.util.Set;
173import java.util.StringTokenizer;
174import java.util.UUID;
175import java.util.concurrent.TimeUnit;
176import java.util.function.Consumer;
177import java.util.function.Supplier;
178import java.util.stream.Collectors;
179
180import static ca.uhn.fhir.jpa.entity.TermConceptPropertyBinder.CONCEPT_PROPERTY_PREFIX_NAME;
181import static ca.uhn.fhir.jpa.term.api.ITermLoaderSvc.LOINC_URI;
182import static java.lang.String.join;
183import static java.util.stream.Collectors.joining;
184import static java.util.stream.Collectors.toList;
185import static java.util.stream.Collectors.toSet;
186import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
187import static org.apache.commons.lang3.StringUtils.defaultString;
188import static org.apache.commons.lang3.StringUtils.isBlank;
189import static org.apache.commons.lang3.StringUtils.isEmpty;
190import static org.apache.commons.lang3.StringUtils.isNoneBlank;
191import static org.apache.commons.lang3.StringUtils.isNotBlank;
192import static org.apache.commons.lang3.StringUtils.lowerCase;
193import static org.apache.commons.lang3.StringUtils.startsWithIgnoreCase;
194
195public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
196        public static final int DEFAULT_FETCH_SIZE = 250;
197        public static final int DEFAULT_MASS_INDEXER_OBJECT_LOADING_THREADS = 2;
198        // doesn't seem to be much gain by using more threads than this value
199        public static final int MAX_MASS_INDEXER_OBJECT_LOADING_THREADS = 6;
200        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TermReadSvcImpl.class);
201        private static final ValueSetExpansionOptions DEFAULT_EXPANSION_OPTIONS = new ValueSetExpansionOptions();
202        private static final TermCodeSystemVersionDetails NO_CURRENT_VERSION = new TermCodeSystemVersionDetails(-1L, null);
203        private static final String OUR_PIPE_CHARACTER = "|";
204        private static final int SECONDS_IN_MINUTE = 60;
205        private static final int INDEXED_ROOTS_LOGGING_COUNT = 50_000;
206        private static final String CS_USERDATA_CURRENT_VERSION = TermReadSvcImpl.class.getName() + "_CS_CURRENT_VERSION";
207        private static final String VS_USERDATA_CURRENT_VERSION = TermReadSvcImpl.class.getName() + "_VS_CURRENT_VERSION";
208        private static Runnable myInvokeOnNextCallForUnitTest;
209        private static boolean ourForceDisableHibernateSearchForUnitTest;
210
211        @Autowired
212        protected DaoRegistry myDaoRegistry;
213
214        @Autowired
215        protected ITermCodeSystemDao myCodeSystemDao;
216
217        @Autowired
218        protected ITermConceptDao myConceptDao;
219
220        @Autowired
221        protected ITermValueSetDao myTermValueSetDao;
222
223        @Autowired
224        protected ITermValueSetConceptDao myValueSetConceptDao;
225
226        @Autowired
227        protected ITermValueSetConceptDesignationDao myValueSetConceptDesignationDao;
228
229        @Autowired
230        protected FhirContext myContext;
231
232        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
233        protected EntityManager myEntityManager;
234
235        private boolean myPreExpandingValueSets = false;
236
237        @Autowired
238        private ITermCodeSystemVersionDao myCodeSystemVersionDao;
239
240        @Autowired
241        private JpaStorageSettings myStorageSettings;
242
243        private TransactionTemplate myTxTemplate;
244
245        @Autowired
246        private PlatformTransactionManager myTransactionManager;
247
248        @Autowired(required = false)
249        private IFulltextSearchSvc myFulltextSearchSvc;
250
251        @Autowired
252        private PlatformTransactionManager myTxManager;
253
254        @Autowired
255        private ITermConceptDao myTermConceptDao;
256
257        @Autowired
258        private ITermValueSetConceptViewDao myTermValueSetConceptViewDao;
259
260        @Autowired
261        private ITermValueSetConceptViewOracleDao myTermValueSetConceptViewOracleDao;
262
263        @Autowired(required = false)
264        private ITermDeferredStorageSvc myDeferredStorageSvc;
265
266        @Autowired
267        private IIdHelperService<JpaPid> myIdHelperService;
268
269        @Autowired
270        private ApplicationContext myApplicationContext;
271
272        private volatile IValidationSupport myJpaValidationSupport;
273        private volatile IValidationSupport myValidationSupport;
274        // We need this bean so we can tell which mode hibernate search is running in.
275        @Autowired
276        private HibernatePropertiesProvider myHibernatePropertiesProvider;
277
278        @Autowired
279        private VersionCanonicalizer myVersionCanonicalizer;
280
281        @Autowired
282        private IJpaStorageResourceParser myJpaStorageResourceParser;
283
284        @Autowired
285        private InMemoryTerminologyServerValidationSupport myInMemoryTerminologyServerValidationSupport;
286
287        @Autowired
288        private ValueSetConceptAccumulatorFactory myValueSetConceptAccumulatorFactory;
289
290        @Autowired
291        private PartitionSettings myPartitionSettings;
292
293        @Override
294        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
295                if (isBlank(theSystem)) {
296                        return false;
297                }
298                TermCodeSystemVersionDetails cs = getCurrentCodeSystemVersion(theValidationSupportContext, theSystem);
299                return cs != null;
300        }
301
302        @Override
303        public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
304                return fetchValueSet(theValueSetUrl) != null;
305        }
306
307        private boolean addCodeIfNotAlreadyAdded(
308                        @Nullable ValueSetExpansionOptions theExpansionOptions,
309                        IValueSetConceptAccumulator theValueSetCodeAccumulator,
310                        Set<String> theAddedCodes,
311                        TermConcept theConcept,
312                        String theConceptDisplayValue,
313                        boolean theAdd,
314                        String theValueSetIncludeVersion) {
315                String codeSystem = theConcept.getCodeSystemVersion().getCodeSystem().getCodeSystemUri();
316                String codeSystemVersion = theConcept.getCodeSystemVersion().getCodeSystemVersionId();
317                String code = theConcept.getCode();
318                Long sourceConceptPid = theConcept.getId();
319                String directParentPids = "";
320
321                if (theExpansionOptions != null && theExpansionOptions.isIncludeHierarchy()) {
322                        directParentPids = theConcept.getParents().stream()
323                                        .map(t -> t.getParent().getId().toString())
324                                        .collect(joining(" "));
325                }
326
327                Collection<TermConceptDesignation> designations = theConcept.getDesignations();
328
329                if (StringUtils.isNotEmpty(theValueSetIncludeVersion)) {
330                        codeSystem = codeSystem + OUR_PIPE_CHARACTER + theValueSetIncludeVersion;
331                }
332
333                return addCodeIfNotAlreadyAdded(
334                                theValueSetCodeAccumulator,
335                                theAddedCodes,
336                                designations,
337                                theAdd,
338                                codeSystem,
339                                code,
340                                theConceptDisplayValue,
341                                sourceConceptPid,
342                                directParentPids,
343                                codeSystemVersion);
344        }
345
346        private boolean addCodeIfNotAlreadyAdded(
347                        IValueSetConceptAccumulator theValueSetCodeAccumulator,
348                        Set<String> theAddedCodes,
349                        boolean theAdd,
350                        String theCodeSystem,
351                        String theCodeSystemVersion,
352                        String theCode,
353                        String theDisplay,
354                        Long theSourceConceptPid,
355                        String theSourceConceptDirectParentPids,
356                        Collection<TermConceptDesignation> theDesignations) {
357                if (StringUtils.isNotEmpty(theCodeSystemVersion)) {
358                        if (isNoneBlank(theCodeSystem, theCode)) {
359                                if (theAdd && theAddedCodes.add(theCodeSystem + OUR_PIPE_CHARACTER + theCode)) {
360                                        theValueSetCodeAccumulator.includeConceptWithDesignations(
361                                                        theCodeSystem + OUR_PIPE_CHARACTER + theCodeSystemVersion,
362                                                        theCode,
363                                                        theDisplay,
364                                                        theDesignations,
365                                                        theSourceConceptPid,
366                                                        theSourceConceptDirectParentPids,
367                                                        theCodeSystemVersion);
368                                        return true;
369                                }
370
371                                if (!theAdd && theAddedCodes.remove(theCodeSystem + OUR_PIPE_CHARACTER + theCode)) {
372                                        theValueSetCodeAccumulator.excludeConcept(
373                                                        theCodeSystem + OUR_PIPE_CHARACTER + theCodeSystemVersion, theCode);
374                                        return true;
375                                }
376                        }
377                } else {
378                        if (theAdd && theAddedCodes.add(theCodeSystem + OUR_PIPE_CHARACTER + theCode)) {
379                                theValueSetCodeAccumulator.includeConceptWithDesignations(
380                                                theCodeSystem,
381                                                theCode,
382                                                theDisplay,
383                                                theDesignations,
384                                                theSourceConceptPid,
385                                                theSourceConceptDirectParentPids,
386                                                theCodeSystemVersion);
387                                return true;
388                        }
389
390                        if (!theAdd && theAddedCodes.remove(theCodeSystem + OUR_PIPE_CHARACTER + theCode)) {
391                                theValueSetCodeAccumulator.excludeConcept(theCodeSystem, theCode);
392                                return true;
393                        }
394                }
395
396                return false;
397        }
398
399        private boolean addCodeIfNotAlreadyAdded(
400                        IValueSetConceptAccumulator theValueSetCodeAccumulator,
401                        Set<String> theAddedCodes,
402                        Collection<TermConceptDesignation> theDesignations,
403                        boolean theAdd,
404                        String theCodeSystem,
405                        String theCode,
406                        String theDisplay,
407                        Long theSourceConceptPid,
408                        String theSourceConceptDirectParentPids,
409                        String theSystemVersion) {
410                if (isNoneBlank(theCodeSystem, theCode)) {
411                        if (theAdd && theAddedCodes.add(theCodeSystem + OUR_PIPE_CHARACTER + theCode)) {
412                                theValueSetCodeAccumulator.includeConceptWithDesignations(
413                                                theCodeSystem,
414                                                theCode,
415                                                theDisplay,
416                                                theDesignations,
417                                                theSourceConceptPid,
418                                                theSourceConceptDirectParentPids,
419                                                theSystemVersion);
420                                return true;
421                        }
422
423                        if (!theAdd && theAddedCodes.remove(theCodeSystem + OUR_PIPE_CHARACTER + theCode)) {
424                                theValueSetCodeAccumulator.excludeConcept(theCodeSystem, theCode);
425                                return true;
426                        }
427                }
428
429                return false;
430        }
431
432        private boolean addToSet(Set<TermConcept> theSetToPopulate, TermConcept theConcept) {
433                boolean retVal = theSetToPopulate.add(theConcept);
434                if (retVal) {
435                        if (theSetToPopulate.size() >= myStorageSettings.getMaximumExpansionSize()) {
436                                String msg = myContext
437                                                .getLocalizer()
438                                                .getMessage(
439                                                                TermReadSvcImpl.class,
440                                                                "expansionTooLarge",
441                                                                myStorageSettings.getMaximumExpansionSize());
442                                throw new ExpansionTooCostlyException(Msg.code(885) + msg);
443                        }
444                }
445                return retVal;
446        }
447
448        public Optional<TermValueSet> deleteValueSetForResource(ResourceTable theResourceTable) {
449                // Get existing entity so it can be deleted.
450                Optional<TermValueSet> optionalExistingTermValueSetById =
451                                myTermValueSetDao.findByResourcePid(theResourceTable.getId());
452
453                if (optionalExistingTermValueSetById.isPresent()) {
454                        TermValueSet existingTermValueSet = optionalExistingTermValueSetById.get();
455
456                        ourLog.info("Deleting existing TermValueSet[{}] and its children...", existingTermValueSet.getId());
457                        deletePreCalculatedValueSetContents(existingTermValueSet);
458                        myTermValueSetDao.deleteById(existingTermValueSet.getPartitionedId());
459
460                        /*
461                         * If we're updating an existing ValueSet within a transaction, we need to make
462                         * sure to manually flush now since otherwise we'll try to create a new
463                         * TermValueSet entity and fail with a constraint error on the URL, since
464                         * this one won't be deleted yet
465                         */
466                        myTermValueSetDao.flush();
467
468                        ourLog.info("Done deleting existing TermValueSet[{}] and its children.", existingTermValueSet.getId());
469                }
470
471                return optionalExistingTermValueSetById;
472        }
473
474        private void deletePreCalculatedValueSetContents(TermValueSet theValueSet) {
475                myValueSetConceptDesignationDao.deleteByTermValueSetId(theValueSet.getId());
476                myValueSetConceptDao.deleteByTermValueSetId(theValueSet.getId());
477        }
478
479        @Override
480        @Transactional
481        public void deleteValueSetAndChildren(ResourceTable theResourceTable) {
482                deleteValueSetForResource(theResourceTable);
483        }
484
485        @Override
486        @Transactional
487        public List<FhirVersionIndependentConcept> expandValueSetIntoConceptList(
488                        @Nullable ValueSetExpansionOptions theExpansionOptions, @Nonnull String theValueSetCanonicalUrl) {
489                // TODO: DM 2019-09-10 - This is problematic because an incorrect URL that matches ValueSet.id will not be found
490                // in the terminology tables but will yield a ValueSet here. Depending on the ValueSet, the expansion may
491                // time-out.
492
493                ValueSet expanded = expandValueSet(theExpansionOptions, theValueSetCanonicalUrl);
494
495                ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>();
496                for (ValueSet.ValueSetExpansionContainsComponent nextContains :
497                                expanded.getExpansion().getContains()) {
498                        retVal.add(new FhirVersionIndependentConcept(
499                                        nextContains.getSystem(),
500                                        nextContains.getCode(),
501                                        nextContains.getDisplay(),
502                                        nextContains.getVersion()));
503                }
504                return retVal;
505        }
506
507        @Override
508        public ValueSet expandValueSet(
509                        @Nullable ValueSetExpansionOptions theExpansionOptions, @Nonnull String theValueSetCanonicalUrl) {
510                ValueSet valueSet = fetchCanonicalValueSetFromCompleteContext(theValueSetCanonicalUrl);
511                if (valueSet == null) {
512                        throw new ResourceNotFoundException(
513                                        Msg.code(886) + "Unknown ValueSet: " + UrlUtil.escapeUrlParam(theValueSetCanonicalUrl));
514                }
515
516                return expandValueSet(theExpansionOptions, valueSet);
517        }
518
519        @Override
520        public ValueSet expandValueSet(
521                        @Nullable ValueSetExpansionOptions theExpansionOptions, @Nonnull ValueSet theValueSetToExpand) {
522                String filter = null;
523                if (theExpansionOptions != null) {
524                        filter = theExpansionOptions.getFilter();
525                }
526                return doExpandValueSet(theExpansionOptions, theValueSetToExpand, ExpansionFilter.fromFilterString(filter));
527        }
528
529        private ValueSet doExpandValueSet(
530                        @Nullable ValueSetExpansionOptions theExpansionOptions,
531                        ValueSet theValueSetToExpand,
532                        ExpansionFilter theFilter) {
533                Set<String> addedCodes = new HashSet<>();
534                ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSetToExpand, "ValueSet to expand can not be null");
535
536                ValueSetExpansionOptions expansionOptions = provideExpansionOptions(theExpansionOptions);
537                int offset = expansionOptions.getOffset();
538                int count = expansionOptions.getCount();
539
540                ValueSetExpansionComponentWithConceptAccumulator accumulator =
541                                new ValueSetExpansionComponentWithConceptAccumulator(
542                                                myContext, count, expansionOptions.isIncludeHierarchy());
543                accumulator.setHardExpansionMaximumSize(myStorageSettings.getMaximumExpansionSize());
544                accumulator.setSkipCountRemaining(offset);
545                accumulator.setIdentifier(UUID.randomUUID().toString());
546                accumulator.setTimestamp(new Date());
547                accumulator.setOffset(offset);
548
549                if (theExpansionOptions != null && isHibernateSearchEnabled()) {
550                        accumulator.addParameter().setName("offset").setValue(new IntegerType(offset));
551                        accumulator.addParameter().setName("count").setValue(new IntegerType(count));
552                }
553
554                myTxTemplate.executeWithoutResult(tx -> expandValueSetIntoAccumulator(
555                                theValueSetToExpand, theExpansionOptions, accumulator, theFilter, true, addedCodes));
556
557                if (accumulator.getTotalConcepts() != null) {
558                        accumulator.setTotal(accumulator.getTotalConcepts());
559                }
560
561                ValueSet valueSet = new ValueSet();
562                valueSet.setUrl(theValueSetToExpand.getUrl());
563                valueSet.setId(theValueSetToExpand.getId());
564                valueSet.setStatus(Enumerations.PublicationStatus.ACTIVE);
565                valueSet.setCompose(theValueSetToExpand.getCompose());
566                valueSet.setExpansion(accumulator);
567
568                for (String next : accumulator.getMessages()) {
569                        valueSet.getMeta()
570                                        .addExtension()
571                                        .setUrl(HapiExtensions.EXT_VALUESET_EXPANSION_MESSAGE)
572                                        .setValue(new StringType(next));
573                }
574
575                if (expansionOptions.isIncludeHierarchy()) {
576                        accumulator.applyHierarchy();
577                }
578
579                return valueSet;
580        }
581
582        private void expandValueSetIntoAccumulator(
583                        ValueSet theValueSetToExpand,
584                        ValueSetExpansionOptions theExpansionOptions,
585                        IValueSetConceptAccumulator theAccumulator,
586                        ExpansionFilter theFilter,
587                        boolean theAdd,
588                        Set<String> theAddedCodes) {
589                Optional<TermValueSet> optionalTermValueSet;
590                if (theValueSetToExpand.hasUrl()) {
591                        if (theValueSetToExpand.hasVersion()) {
592                                optionalTermValueSet = myTermValueSetDao.findTermValueSetByUrlAndVersion(
593                                                theValueSetToExpand.getUrl(), theValueSetToExpand.getVersion());
594                        } else {
595                                optionalTermValueSet = findCurrentTermValueSet(theValueSetToExpand.getUrl());
596                        }
597                } else {
598                        optionalTermValueSet = Optional.empty();
599                }
600
601                /*
602                 * ValueSet doesn't exist in pre-expansion database, so perform in-memory expansion
603                 */
604                if (optionalTermValueSet.isEmpty()) {
605                        ourLog.debug(
606                                        "ValueSet is not present in terminology tables. Will perform in-memory expansion without parameters. {}",
607                                        getValueSetInfo(theValueSetToExpand));
608                        String msg = myContext
609                                        .getLocalizer()
610                                        .getMessage(
611                                                        TermReadSvcImpl.class,
612                                                        "valueSetExpandedUsingInMemoryExpansion",
613                                                        getValueSetInfo(theValueSetToExpand));
614                        theAccumulator.addMessage(msg);
615                        doExpandValueSet(theExpansionOptions, theValueSetToExpand, theAccumulator, theFilter, theAddedCodes);
616                        return;
617                }
618
619                /*
620                 * ValueSet exists in pre-expansion database, but pre-expansion is not yet complete so perform in-memory expansion
621                 */
622                TermValueSet termValueSet = optionalTermValueSet.get();
623                if (termValueSet.getExpansionStatus() != TermValueSetPreExpansionStatusEnum.EXPANDED) {
624                        String msg = myContext
625                                        .getLocalizer()
626                                        .getMessage(
627                                                        TermReadSvcImpl.class,
628                                                        "valueSetNotYetExpanded",
629                                                        getValueSetInfo(theValueSetToExpand),
630                                                        termValueSet.getExpansionStatus().name(),
631                                                        termValueSet.getExpansionStatus().getDescription());
632                        theAccumulator.addMessage(msg);
633                        doExpandValueSet(theExpansionOptions, theValueSetToExpand, theAccumulator, theFilter, theAddedCodes);
634                        return;
635                }
636
637                /*
638                 * ValueSet is pre-expanded in database so let's use that
639                 */
640                String expansionTimestamp = toHumanReadableExpansionTimestamp(termValueSet);
641                String msg = myContext
642                                .getLocalizer()
643                                .getMessage(TermReadSvcImpl.class, "valueSetExpandedUsingPreExpansion", expansionTimestamp);
644                theAccumulator.addMessage(msg);
645                expandConcepts(
646                                theExpansionOptions,
647                                theAccumulator,
648                                termValueSet,
649                                theFilter,
650                                theAdd,
651                                theAddedCodes,
652                                myHibernatePropertiesProvider.isOracleDialect());
653        }
654
655        @Nonnull
656        private String toHumanReadableExpansionTimestamp(TermValueSet termValueSet) {
657                String expansionTimestamp = "(unknown)";
658                if (termValueSet.getExpansionTimestamp() != null) {
659                        // Note: We used to append "123ms ago" to the timestamp, but we cache the
660                        // results here, so it's just kind of weird to do that since the duration will
661                        // be out of date when the entry comes back from cache
662                        expansionTimestamp = new InstantType(termValueSet.getExpansionTimestamp()).getValueAsString();
663                }
664                return expansionTimestamp;
665        }
666
667        private void expandConcepts(
668                        ValueSetExpansionOptions theExpansionOptions,
669                        IValueSetConceptAccumulator theAccumulator,
670                        TermValueSet theTermValueSet,
671                        ExpansionFilter theFilter,
672                        boolean theAdd,
673                        Set<String> theAddedCodes,
674                        boolean theOracle) {
675                // NOTE: if you modifiy the logic here, look to `expandConceptsOracle` and see if your new code applies to its
676                // copy pasted sibling
677                Integer offset = theAccumulator.getSkipCountRemaining();
678                offset = ObjectUtils.defaultIfNull(offset, 0);
679                offset = Math.min(offset, theTermValueSet.getTotalConcepts().intValue());
680
681                Integer count = theAccumulator.getCapacityRemaining();
682                count = defaultIfNull(count, myStorageSettings.getMaximumExpansionSize());
683
684                int conceptsExpanded = 0;
685                int designationsExpanded = 0;
686                int toIndex = offset + count;
687
688                Collection<? extends ITermValueSetConceptView> conceptViews;
689                boolean wasFilteredResult = false;
690                String filterDisplayValue = null;
691                if (!theFilter.getFilters().isEmpty()
692                                && JpaConstants.VALUESET_FILTER_DISPLAY.equals(
693                                                theFilter.getFilters().get(0).getProperty())
694                                && theFilter.getFilters().get(0).getOp() == ValueSet.FilterOperator.EQUAL) {
695                        filterDisplayValue =
696                                        lowerCase(theFilter.getFilters().get(0).getValue().replace("%", "[%]"));
697                        String displayValue = "%" + lowerCase(filterDisplayValue) + "%";
698                        if (theOracle) {
699                                conceptViews =
700                                                myTermValueSetConceptViewOracleDao.findByTermValueSetId(theTermValueSet.getId(), displayValue);
701                        } else {
702                                conceptViews = myTermValueSetConceptViewDao.findByTermValueSetId(theTermValueSet.getId(), displayValue);
703                        }
704                        wasFilteredResult = true;
705                } else {
706                        if (theOracle) {
707                                conceptViews = myTermValueSetConceptViewOracleDao.findByTermValueSetId(
708                                                offset, toIndex, theTermValueSet.getId());
709                        } else {
710                                conceptViews =
711                                                myTermValueSetConceptViewDao.findByTermValueSetId(offset, toIndex, theTermValueSet.getId());
712                        }
713                        theAccumulator.consumeSkipCount(offset);
714                        if (theAdd) {
715                                theAccumulator.incrementOrDecrementTotalConcepts(
716                                                true, theTermValueSet.getTotalConcepts().intValue());
717                        }
718                }
719
720                if (conceptViews.isEmpty()) {
721                        logConceptsExpanded("No concepts to expand. ", theTermValueSet, conceptsExpanded);
722                        return;
723                }
724
725                Map<Long, FhirVersionIndependentConcept> pidToConcept = new LinkedHashMap<>();
726                ArrayListMultimap<Long, TermConceptDesignation> pidToDesignations = ArrayListMultimap.create();
727                Map<Long, Long> pidToSourcePid = new HashMap<>();
728                Map<Long, String> pidToSourceDirectParentPids = new HashMap<>();
729
730                for (ITermValueSetConceptView conceptView : conceptViews) {
731
732                        String system = conceptView.getConceptSystemUrl();
733                        String code = conceptView.getConceptCode();
734                        String display = conceptView.getConceptDisplay();
735                        String systemVersion = conceptView.getConceptSystemVersion();
736
737                        // -- this is quick solution, may need to revisit
738                        if (!applyFilter(display, filterDisplayValue)) {
739                                continue;
740                        }
741
742                        Long conceptPid = conceptView.getConceptPid();
743                        if (!pidToConcept.containsKey(conceptPid)) {
744                                FhirVersionIndependentConcept concept =
745                                                new FhirVersionIndependentConcept(system, code, display, systemVersion);
746                                pidToConcept.put(conceptPid, concept);
747                        }
748
749                        // TODO: DM 2019-08-17 - Implement includeDesignations parameter for $expand operation to designations
750                        // optional.
751                        if (conceptView.getDesignationPid() != null) {
752                                TermConceptDesignation designation = new TermConceptDesignation();
753
754                                if (isValueSetDisplayLanguageMatch(theExpansionOptions, conceptView.getDesignationLang())) {
755                                        designation.setUseSystem(conceptView.getDesignationUseSystem());
756                                        designation.setUseCode(conceptView.getDesignationUseCode());
757                                        designation.setUseDisplay(conceptView.getDesignationUseDisplay());
758                                        designation.setValue(conceptView.getDesignationVal());
759                                        designation.setLanguage(conceptView.getDesignationLang());
760                                        pidToDesignations.put(conceptPid, designation);
761                                }
762
763                                if (++designationsExpanded % 250 == 0) {
764                                        logDesignationsExpanded(
765                                                        "Expansion of designations in progress. ", theTermValueSet, designationsExpanded);
766                                }
767                        }
768
769                        if (theAccumulator.isTrackingHierarchy()) {
770                                pidToSourcePid.put(conceptPid, conceptView.getSourceConceptPid());
771                                pidToSourceDirectParentPids.put(conceptPid, conceptView.getSourceConceptDirectParentPids());
772                        }
773
774                        if (++conceptsExpanded % 250 == 0) {
775                                logConceptsExpanded("Expansion of concepts in progress. ", theTermValueSet, conceptsExpanded);
776                        }
777                }
778
779                for (Long nextPid : pidToConcept.keySet()) {
780                        FhirVersionIndependentConcept concept = pidToConcept.get(nextPid);
781                        List<TermConceptDesignation> designations = pidToDesignations.get(nextPid);
782                        String system = concept.getSystem();
783                        String code = concept.getCode();
784                        String display = concept.getDisplay();
785                        String systemVersion = concept.getSystemVersion();
786
787                        if (theAdd) {
788                                if (theAccumulator.getCapacityRemaining() != null) {
789                                        if (theAccumulator.getCapacityRemaining() == 0) {
790                                                break;
791                                        }
792                                }
793
794                                Long sourceConceptPid = pidToSourcePid.get(nextPid);
795                                String sourceConceptDirectParentPids = pidToSourceDirectParentPids.get(nextPid);
796                                if (theAddedCodes.add(system + OUR_PIPE_CHARACTER + code)) {
797                                        theAccumulator.includeConceptWithDesignations(
798                                                        system,
799                                                        code,
800                                                        display,
801                                                        designations,
802                                                        sourceConceptPid,
803                                                        sourceConceptDirectParentPids,
804                                                        systemVersion);
805                                        if (wasFilteredResult) {
806                                                theAccumulator.incrementOrDecrementTotalConcepts(true, 1);
807                                        }
808                                }
809                        } else {
810                                if (theAddedCodes.remove(system + OUR_PIPE_CHARACTER + code)) {
811                                        theAccumulator.excludeConcept(system, code);
812                                        theAccumulator.incrementOrDecrementTotalConcepts(false, 1);
813                                }
814                        }
815                }
816
817                logDesignationsExpanded("Finished expanding designations. ", theTermValueSet, designationsExpanded);
818                logConceptsExpanded("Finished expanding concepts. ", theTermValueSet, conceptsExpanded);
819        }
820
821        private void logConceptsExpanded(
822                        String theLogDescriptionPrefix, TermValueSet theTermValueSet, int theConceptsExpanded) {
823                if (theConceptsExpanded > 0) {
824                        ourLog.debug(
825                                        "{}Have expanded {} concepts in ValueSet[{}]",
826                                        theLogDescriptionPrefix,
827                                        theConceptsExpanded,
828                                        theTermValueSet.getUrl());
829                }
830        }
831
832        private void logDesignationsExpanded(
833                        String theLogDescriptionPrefix, TermValueSet theTermValueSet, int theDesignationsExpanded) {
834                if (theDesignationsExpanded > 0) {
835                        ourLog.debug(
836                                        "{}Have expanded {} designations in ValueSet[{}]",
837                                        theLogDescriptionPrefix,
838                                        theDesignationsExpanded,
839                                        theTermValueSet.getUrl());
840                }
841        }
842
843        public boolean applyFilter(final String theDisplay, final String theFilterDisplay) {
844
845                // -- safety check only, no need to apply filter
846                if (theDisplay == null || theFilterDisplay == null) return true;
847
848                // -- sentence case
849                if (startsWithIgnoreCase(theDisplay, theFilterDisplay)) return true;
850
851                // -- token case
852                return startsWithByWordBoundaries(theDisplay, theFilterDisplay);
853        }
854
855        private boolean startsWithByWordBoundaries(String theDisplay, String theFilterDisplay) {
856                // return true only e.g. the input is 'Body height', theFilterDisplay is "he", or 'bo'
857                StringTokenizer tok = new StringTokenizer(theDisplay);
858                List<String> tokens = new ArrayList<>();
859                while (tok.hasMoreTokens()) {
860                        String token = tok.nextToken();
861                        if (startsWithIgnoreCase(token, theFilterDisplay)) return true;
862                        tokens.add(token);
863                }
864
865                // Allow to search by the end of the phrase.  E.g.  "working proficiency" will match "Limited working
866                // proficiency"
867                for (int start = 0; start <= tokens.size() - 1; ++start) {
868                        for (int end = start + 1; end <= tokens.size(); ++end) {
869                                String sublist = String.join(" ", tokens.subList(start, end));
870                                if (startsWithIgnoreCase(sublist, theFilterDisplay)) return true;
871                        }
872                }
873                return false;
874        }
875
876        @Override
877        public void expandValueSet(
878                        ValueSetExpansionOptions theExpansionOptions,
879                        ValueSet theValueSetToExpand,
880                        IValueSetConceptAccumulator theValueSetCodeAccumulator) {
881                Set<String> addedCodes = new HashSet<>();
882                doExpandValueSet(
883                                theExpansionOptions,
884                                theValueSetToExpand,
885                                theValueSetCodeAccumulator,
886                                ExpansionFilter.NO_FILTER,
887                                addedCodes);
888        }
889
890        /**
891         * Note: Not transactional because specific calls within this method
892         * get executed in a transaction
893         */
894        private void doExpandValueSet(
895                        ValueSetExpansionOptions theExpansionOptions,
896                        ValueSet theValueSetToExpand,
897                        IValueSetConceptAccumulator theValueSetCodeAccumulator,
898                        @Nonnull ExpansionFilter theExpansionFilter,
899                        Set<String> theAddedCodes) {
900
901                StopWatch sw = new StopWatch();
902                String valueSetInfo = getValueSetInfo(theValueSetToExpand);
903                ourLog.debug("Working with {}", valueSetInfo);
904
905                // Offset can't be combined with excludes
906                Integer skipCountRemaining = theValueSetCodeAccumulator.getSkipCountRemaining();
907                if (skipCountRemaining != null && skipCountRemaining > 0) {
908                        if (!theValueSetToExpand.getCompose().getExclude().isEmpty()) {
909                                String msg = myContext
910                                                .getLocalizer()
911                                                .getMessage(TermReadSvcImpl.class, "valueSetNotYetExpanded_OffsetNotAllowed", valueSetInfo);
912                                throw new InvalidRequestException(Msg.code(887) + msg);
913                        }
914                }
915
916                // Handle includes
917                ourLog.debug("Handling includes");
918                for (ValueSet.ConceptSetComponent include :
919                                theValueSetToExpand.getCompose().getInclude()) {
920                        myTxTemplate.executeWithoutResult(tx -> expandValueSetHandleIncludeOrExclude(
921                                        theExpansionOptions, theValueSetCodeAccumulator, theAddedCodes, include, true, theExpansionFilter));
922                }
923
924                // Handle excludes
925                ourLog.debug("Handling excludes");
926                for (ValueSet.ConceptSetComponent exclude :
927                                theValueSetToExpand.getCompose().getExclude()) {
928                        myTxTemplate.executeWithoutResult(tx -> expandValueSetHandleIncludeOrExclude(
929                                        theExpansionOptions,
930                                        theValueSetCodeAccumulator,
931                                        theAddedCodes,
932                                        exclude,
933                                        false,
934                                        ExpansionFilter.NO_FILTER));
935                }
936
937                if (theValueSetCodeAccumulator instanceof ValueSetConceptAccumulator) {
938                        myTxTemplate.execute(
939                                        t -> ((ValueSetConceptAccumulator) theValueSetCodeAccumulator).removeGapsFromConceptOrder());
940                }
941
942                ourLog.debug("Done working with {} in {}ms", valueSetInfo, sw.getMillis());
943        }
944
945        private String getValueSetInfo(ValueSet theValueSet) {
946                StringBuilder sb = new StringBuilder();
947                boolean isIdentified = false;
948                if (theValueSet.hasUrl()) {
949                        isIdentified = true;
950                        sb.append("ValueSet.url[").append(theValueSet.getUrl()).append("]");
951                } else if (theValueSet.hasId()) {
952                        isIdentified = true;
953                        sb.append("ValueSet.id[").append(theValueSet.getId()).append("]");
954                }
955
956                if (!isIdentified) {
957                        sb.append("Unidentified ValueSet");
958                }
959
960                return sb.toString();
961        }
962
963        /**
964         * Returns true if there are potentially more results to process.
965         */
966        private void expandValueSetHandleIncludeOrExclude(
967                        @Nullable ValueSetExpansionOptions theExpansionOptions,
968                        IValueSetConceptAccumulator theValueSetCodeAccumulator,
969                        Set<String> theAddedCodes,
970                        ValueSet.ConceptSetComponent theIncludeOrExclude,
971                        boolean theAdd,
972                        @Nonnull ExpansionFilter theExpansionFilter) {
973
974                String system = theIncludeOrExclude.getSystem();
975                boolean hasSystem = isNotBlank(system);
976                boolean hasValueSet = !theIncludeOrExclude.getValueSet().isEmpty();
977
978                if (hasSystem) {
979
980                        if (theExpansionFilter.hasCode()
981                                        && theExpansionFilter.getSystem() != null
982                                        && !system.equals(theExpansionFilter.getSystem())) {
983                                return;
984                        }
985
986                        ourLog.debug("Starting {} expansion around CodeSystem: {}", (theAdd ? "inclusion" : "exclusion"), system);
987
988                        Optional<TermCodeSystemVersion> termCodeSystemVersion =
989                                        optionalFindTermCodeSystemVersion(theIncludeOrExclude);
990                        if (termCodeSystemVersion.isPresent()) {
991
992                                expandValueSetHandleIncludeOrExcludeUsingDatabase(
993                                                theExpansionOptions,
994                                                theValueSetCodeAccumulator,
995                                                theAddedCodes,
996                                                theIncludeOrExclude,
997                                                theAdd,
998                                                theExpansionFilter,
999                                                system,
1000                                                termCodeSystemVersion.get());
1001
1002                        } else {
1003
1004                                if (!theIncludeOrExclude.getConcept().isEmpty() && theExpansionFilter.hasCode()) {
1005                                        if (defaultString(theIncludeOrExclude.getSystem()).equals(theExpansionFilter.getSystem())) {
1006                                                if (theIncludeOrExclude.getConcept().stream()
1007                                                                .noneMatch(t -> t.getCode().equals(theExpansionFilter.getCode()))) {
1008                                                        return;
1009                                                }
1010                                        }
1011                                }
1012
1013                                Consumer<FhirVersionIndependentConcept> consumer = c -> addOrRemoveCode(
1014                                                theValueSetCodeAccumulator,
1015                                                theAddedCodes,
1016                                                theAdd,
1017                                                system,
1018                                                c.getCode(),
1019                                                c.getDisplay(),
1020                                                c.getSystemVersion());
1021
1022                                try {
1023                                        ConversionContext40_50.INSTANCE.init(
1024                                                        new VersionConvertor_40_50(new BaseAdvisor_40_50()), "ValueSet");
1025                                        org.hl7.fhir.r5.model.ValueSet.ConceptSetComponent includeOrExclude =
1026                                                        ValueSet40_50.convertConceptSetComponent(theIncludeOrExclude);
1027                                        myInMemoryTerminologyServerValidationSupport.expandValueSetIncludeOrExclude(
1028                                                        new ValidationSupportContext(provideValidationSupport()), consumer, includeOrExclude);
1029                                } catch (InMemoryTerminologyServerValidationSupport.ExpansionCouldNotBeCompletedInternallyException e) {
1030                                        if (theExpansionOptions != null
1031                                                        && !theExpansionOptions.isFailOnMissingCodeSystem()
1032                                                        // Code system is unknown, therefore NOT_FOUND
1033                                                        && e.getCodeValidationIssue()
1034                                                                        .hasIssueDetailCode(CodeValidationIssueCoding.NOT_FOUND.getCode())) {
1035                                                return;
1036                                        }
1037                                        throw new InternalErrorException(Msg.code(888) + e);
1038                                } finally {
1039                                        ConversionContext40_50.INSTANCE.close("ValueSet");
1040                                }
1041                        }
1042
1043                } else if (hasValueSet) {
1044
1045                        for (CanonicalType nextValueSet : theIncludeOrExclude.getValueSet()) {
1046                                String valueSetUrl = nextValueSet.getValueAsString();
1047                                ourLog.debug(
1048                                                "Starting {} expansion around ValueSet: {}", (theAdd ? "inclusion" : "exclusion"), valueSetUrl);
1049
1050                                ExpansionFilter subExpansionFilter = new ExpansionFilter(
1051                                                theExpansionFilter,
1052                                                theIncludeOrExclude.getFilter(),
1053                                                theValueSetCodeAccumulator.getCapacityRemaining());
1054
1055                                // TODO: DM 2019-09-10 - This is problematic because an incorrect URL that matches ValueSet.id will not
1056                                // be found in the terminology tables but will yield a ValueSet here. Depending on the ValueSet, the
1057                                // expansion may time-out.
1058
1059                                ValueSet valueSet = fetchCanonicalValueSetFromCompleteContext(valueSetUrl);
1060                                if (valueSet == null) {
1061                                        throw new ResourceNotFoundException(
1062                                                        Msg.code(889) + "Unknown ValueSet: " + UrlUtil.escapeUrlParam(valueSetUrl));
1063                                }
1064
1065                                expandValueSetIntoAccumulator(
1066                                                valueSet,
1067                                                theExpansionOptions,
1068                                                theValueSetCodeAccumulator,
1069                                                subExpansionFilter,
1070                                                theAdd,
1071                                                theAddedCodes);
1072                        }
1073
1074                } else {
1075                        throw new InvalidRequestException(Msg.code(890) + "ValueSet contains " + (theAdd ? "include" : "exclude")
1076                                        + " criteria with no system defined");
1077                }
1078        }
1079
1080        private Optional<TermCodeSystemVersion> optionalFindTermCodeSystemVersion(
1081                        ValueSet.ConceptSetComponent theIncludeOrExclude) {
1082                if (isEmpty(theIncludeOrExclude.getVersion())) {
1083                        return Optional.ofNullable(myCodeSystemDao.findByCodeSystemUri(theIncludeOrExclude.getSystem()))
1084                                        .map(TermCodeSystem::getCurrentVersion);
1085                } else {
1086                        return Optional.ofNullable(myCodeSystemVersionDao.findByCodeSystemUriAndVersion(
1087                                        theIncludeOrExclude.getSystem(), theIncludeOrExclude.getVersion()));
1088                }
1089        }
1090
1091        private boolean isHibernateSearchEnabled() {
1092                return myFulltextSearchSvc != null && !ourForceDisableHibernateSearchForUnitTest;
1093        }
1094
1095        private void expandValueSetHandleIncludeOrExcludeUsingDatabase(
1096                        ValueSetExpansionOptions theExpansionOptions,
1097                        IValueSetConceptAccumulator theValueSetCodeAccumulator,
1098                        Set<String> theAddedCodes,
1099                        ValueSet.ConceptSetComponent theIncludeOrExclude,
1100                        boolean theAdd,
1101                        @Nonnull ExpansionFilter theExpansionFilter,
1102                        String theSystem,
1103                        TermCodeSystemVersion theTermCodeSystemVersion) {
1104
1105                StopWatch fullOperationSw = new StopWatch();
1106                String includeOrExcludeVersion = theIncludeOrExclude.getVersion();
1107
1108                /*
1109                 * If FullText searching is not enabled, we can handle only basic expansions
1110                 * since we're going to do it without the database.
1111                 */
1112                if (!isHibernateSearchEnabled()) {
1113                        expandWithoutHibernateSearch(
1114                                        theValueSetCodeAccumulator,
1115                                        theTermCodeSystemVersion,
1116                                        theAddedCodes,
1117                                        theIncludeOrExclude,
1118                                        theSystem,
1119                                        theAdd);
1120                        return;
1121                }
1122
1123                /*
1124                 * Ok, let's use hibernate search to build the expansion
1125                 */
1126
1127                int count = 0;
1128
1129                Optional<Integer> chunkSizeOpt = getScrollChunkSize(theAdd, theValueSetCodeAccumulator);
1130                if (chunkSizeOpt.isEmpty()) {
1131                        return;
1132                }
1133                int chunkSize = chunkSizeOpt.get();
1134
1135                /*
1136                 * Turn the filter into one or more Hibernate Search queries. Ideally we want it
1137                 * to be handled by a single query, but Lucene/ES don't like it when we exceed
1138                 * 1024 different terms in a single query. So if we have that many terms (which
1139                 * can happen if a ValueSet has a lot of explicitly enumerated codes that it's
1140                 * including) we split this into multiple searches. The method below builds these
1141                 * searches lazily, returning a Supplier that creates and executes the search
1142                 * when it's actually time to.
1143                 */
1144                SearchProperties searchProps = buildSearchScrolls(
1145                                theTermCodeSystemVersion,
1146                                theExpansionFilter,
1147                                theSystem,
1148                                theIncludeOrExclude,
1149                                chunkSize,
1150                                includeOrExcludeVersion);
1151                int accumulatedBatchesSoFar = 0;
1152                for (var next : searchProps.getSearchScroll()) {
1153                        try (SearchScroll<EntityReference> scroll = next.get()) {
1154                                ourLog.debug(
1155                                                "Beginning batch expansion for {} with max results per batch: {}",
1156                                                (theAdd ? "inclusion" : "exclusion"),
1157                                                chunkSize);
1158                                for (SearchScrollResult<EntityReference> chunk = scroll.next();
1159                                                chunk.hasHits();
1160                                                chunk = scroll.next()) {
1161                                        int countForBatch = 0;
1162
1163                                        List<TermConcept.TermConceptPk> pids = chunk.hits().stream()
1164                                                        .map(t -> (TermConcept.TermConceptPk) t.id())
1165                                                        .collect(Collectors.toList());
1166
1167                                        /*
1168                                         * This is a (hopefully) temporary hack - we are pulling concepts
1169                                         * out of ElasticSearch here and ES concepts aren't yet partition
1170                                         * aware. So we'll assume that they meant the default partition. This
1171                                         * will always work for now since terminology stuff is non-partitionable
1172                                         * but that could change in the future.
1173                                         */
1174                                        for (var pid : pids) {
1175                                                if (pid.getPartitionIdValue() == null) {
1176                                                        pid.setPartitionIdValue(myPartitionSettings.getDefaultPartitionId());
1177                                                }
1178                                        }
1179
1180                                        List<TermConcept> termConcepts = myTermConceptDao.fetchConceptsAndDesignationsByPid(pids);
1181
1182                                        // If the include section had multiple codes, return the codes in the same order
1183                                        termConcepts = sortTermConcepts(searchProps, termConcepts);
1184
1185                                        //       int firstResult = theQueryIndex * maxResultsPerBatch;
1186                                        // TODO GGG HS we lose the ability to check the
1187                                        // index of the first result, so just best-guessing it here.
1188                                        int delta = 0;
1189                                        for (TermConcept concept : termConcepts) {
1190                                                count++;
1191                                                countForBatch++;
1192                                                String conceptDisplayValue = concept.getDisplay();
1193                                                if (theAdd && searchProps.hasIncludeOrExcludeCodes()) {
1194                                                        ValueSet.ConceptReferenceComponent theIncludeConcept =
1195                                                                        getMatchedConceptIncludedInValueSet(theIncludeOrExclude, concept);
1196                                                        if (theIncludeConcept != null && isNotBlank(theIncludeConcept.getDisplay())) {
1197                                                                conceptDisplayValue = theIncludeConcept.getDisplay();
1198                                                        }
1199                                                }
1200                                                boolean added = addCodeIfNotAlreadyAdded(
1201                                                                theExpansionOptions,
1202                                                                theValueSetCodeAccumulator,
1203                                                                theAddedCodes,
1204                                                                concept,
1205                                                                conceptDisplayValue,
1206                                                                theAdd,
1207                                                                includeOrExcludeVersion);
1208                                                if (added) {
1209                                                        delta++;
1210                                                }
1211                                        }
1212
1213                                        ourLog.debug(
1214                                                        "Batch expansion scroll for {} with offset {} produced {} results in {}ms",
1215                                                        (theAdd ? "inclusion" : "exclusion"),
1216                                                        accumulatedBatchesSoFar,
1217                                                        chunk.hits().size(),
1218                                                        chunk.took().toMillis());
1219
1220                                        theValueSetCodeAccumulator.incrementOrDecrementTotalConcepts(theAdd, delta);
1221                                        accumulatedBatchesSoFar += countForBatch;
1222
1223                                        // keep session bounded
1224                                        myEntityManager.flush();
1225                                        myEntityManager.clear();
1226                                }
1227
1228                                ourLog.debug(
1229                                                "Expansion for {} produced {} results in {}ms",
1230                                                (theAdd ? "inclusion" : "exclusion"),
1231                                                count,
1232                                                fullOperationSw.getMillis());
1233                        }
1234                }
1235        }
1236
1237        private List<TermConcept> sortTermConcepts(SearchProperties searchProps, List<TermConcept> termConcepts) {
1238                List<String> codes = searchProps.getIncludeOrExcludeCodes();
1239                if (codes.size() > 1) {
1240                        termConcepts = new ArrayList<>(termConcepts);
1241                        Map<String, Integer> codeToIndex = new HashMap<>(codes.size());
1242                        for (int i = 0; i < codes.size(); i++) {
1243                                codeToIndex.put(codes.get(i), i);
1244                        }
1245                        termConcepts.sort(((o1, o2) -> {
1246                                Integer idx1 = codeToIndex.get(o1.getCode());
1247                                Integer idx2 = codeToIndex.get(o2.getCode());
1248                                return Comparators.nullsHigh().compare(idx1, idx2);
1249                        }));
1250                }
1251                return termConcepts;
1252        }
1253
1254        private Optional<Integer> getScrollChunkSize(
1255                        boolean theAdd, IValueSetConceptAccumulator theValueSetCodeAccumulator) {
1256                int maxResultsPerBatch = SearchBuilder.getMaximumPageSize();
1257
1258                /*
1259                 * If the accumulator is bounded, we may reduce the size of the query to
1260                 * Lucene in order to be more efficient.
1261                 */
1262                if (theAdd) {
1263                        Integer accumulatorCapacityRemaining = theValueSetCodeAccumulator.getCapacityRemaining();
1264                        if (accumulatorCapacityRemaining != null) {
1265                                maxResultsPerBatch = Math.min(maxResultsPerBatch, accumulatorCapacityRemaining + 1);
1266                        }
1267                }
1268                return maxResultsPerBatch > 0 ? Optional.of(maxResultsPerBatch) : Optional.empty();
1269        }
1270
1271        private SearchProperties buildSearchScrolls(
1272                        TermCodeSystemVersion theTermCodeSystemVersion,
1273                        ExpansionFilter theExpansionFilter,
1274                        String theSystem,
1275                        ValueSet.ConceptSetComponent theIncludeOrExclude,
1276                        Integer theScrollChunkSize,
1277                        String theIncludeOrExcludeVersion) {
1278                SearchSession searchSession = Search.session(myEntityManager);
1279                // Manually building a predicate since we need to throw it around.
1280                SearchPredicateFactory predicate =
1281                                searchSession.scope(TermConcept.class).predicate();
1282
1283                List<String> allCodes = theIncludeOrExclude.getConcept().stream()
1284                                .filter(Objects::nonNull)
1285                                .map(ValueSet.ConceptReferenceComponent::getCode)
1286                                .filter(StringUtils::isNotBlank)
1287                                .collect(Collectors.toList());
1288                SearchProperties returnProps = new SearchProperties();
1289                returnProps.setIncludeOrExcludeCodes(allCodes);
1290
1291                /*
1292                 * Lucene/ES can't typically handle more than 1024 clauses per search, so if
1293                 * we have more than that number (e.g. because of a ValueSet that explicitly
1294                 * includes thousands of codes), we break this up into multiple searches.
1295                 */
1296                List<List<String>> partitionedCodes = ListUtils.partition(allCodes, IndexSearcher.getMaxClauseCount() - 10);
1297                if (partitionedCodes.isEmpty()) {
1298                        partitionedCodes = List.of(List.of());
1299                }
1300
1301                for (List<String> nextCodePartition : partitionedCodes) {
1302                        Supplier<SearchScroll<EntityReference>> nextScroll = () -> {
1303                                // Build the top-level expansion on filters.
1304                                PredicateFinalStep step = predicate.bool(b -> {
1305                                        b.must(predicate
1306                                                        .match()
1307                                                        .field("myCodeSystemVersionPid")
1308                                                        .matching(theTermCodeSystemVersion.getPid()));
1309
1310                                        if (theExpansionFilter.hasCode()) {
1311                                                b.must(predicate.match().field("myCode").matching(theExpansionFilter.getCode()));
1312                                        }
1313
1314                                        String codeSystemUrlAndVersion =
1315                                                        buildCodeSystemUrlAndVersion(theSystem, theIncludeOrExcludeVersion);
1316                                        for (ValueSet.ConceptSetFilterComponent nextFilter : theIncludeOrExclude.getFilter()) {
1317                                                handleFilter(codeSystemUrlAndVersion, predicate, b, nextFilter);
1318                                        }
1319                                        for (ValueSet.ConceptSetFilterComponent nextFilter : theExpansionFilter.getFilters()) {
1320                                                handleFilter(codeSystemUrlAndVersion, predicate, b, nextFilter);
1321                                        }
1322                                });
1323
1324                                // Add a selector on any explicitly enumerated codes in the VS component
1325                                final PredicateFinalStep finishedQuery;
1326                                if (nextCodePartition.isEmpty()) {
1327                                        finishedQuery = step;
1328                                } else {
1329                                        PredicateFinalStep expansionStep = buildExpansionPredicate(nextCodePartition, predicate);
1330                                        finishedQuery = predicate.bool().must(step).must(expansionStep);
1331                                }
1332
1333                                SearchQuery<EntityReference> termConceptsQuery = searchSession
1334                                                .search(TermConcept.class)
1335                                                .selectEntityReference()
1336                                                .where(f -> finishedQuery)
1337                                                .toQuery();
1338
1339                                return termConceptsQuery.scroll(theScrollChunkSize);
1340                        };
1341
1342                        returnProps.addSearchScroll(nextScroll);
1343                }
1344
1345                return returnProps;
1346        }
1347
1348        private ValueSet.ConceptReferenceComponent getMatchedConceptIncludedInValueSet(
1349                        ValueSet.ConceptSetComponent theIncludeOrExclude, TermConcept concept) {
1350                return theIncludeOrExclude.getConcept().stream()
1351                                .filter(includedConcept -> includedConcept.getCode().equalsIgnoreCase(concept.getCode()))
1352                                .findFirst()
1353                                .orElse(null);
1354        }
1355
1356        /**
1357         * Helper method which builds a predicate for the expansion
1358         */
1359        private PredicateFinalStep buildExpansionPredicate(List<String> theCodes, SearchPredicateFactory thePredicate) {
1360                assert !theCodes.isEmpty();
1361                return thePredicate.simpleQueryString().field("myCode").matching(String.join(" | ", theCodes));
1362        }
1363
1364        private String buildCodeSystemUrlAndVersion(String theSystem, String theIncludeOrExcludeVersion) {
1365                String codeSystemUrlAndVersion;
1366                if (theIncludeOrExcludeVersion != null) {
1367                        codeSystemUrlAndVersion = theSystem + OUR_PIPE_CHARACTER + theIncludeOrExcludeVersion;
1368                } else {
1369                        codeSystemUrlAndVersion = theSystem;
1370                }
1371                return codeSystemUrlAndVersion;
1372        }
1373
1374        private @Nonnull ValueSetExpansionOptions provideExpansionOptions(
1375                        @Nullable ValueSetExpansionOptions theExpansionOptions) {
1376                return Objects.requireNonNullElse(theExpansionOptions, DEFAULT_EXPANSION_OPTIONS);
1377        }
1378
1379        private void addOrRemoveCode(
1380                        IValueSetConceptAccumulator theValueSetCodeAccumulator,
1381                        Set<String> theAddedCodes,
1382                        boolean theAdd,
1383                        String theSystem,
1384                        String theCode,
1385                        String theDisplay,
1386                        String theSystemVersion) {
1387                if (theAdd && theAddedCodes.add(theSystem + OUR_PIPE_CHARACTER + theCode)) {
1388                        theValueSetCodeAccumulator.includeConcept(theSystem, theCode, theDisplay, null, null, theSystemVersion);
1389                }
1390                if (!theAdd && theAddedCodes.remove(theSystem + OUR_PIPE_CHARACTER + theCode)) {
1391                        theValueSetCodeAccumulator.excludeConcept(theSystem, theCode);
1392                }
1393        }
1394
1395        private void handleFilter(
1396                        String theCodeSystemIdentifier,
1397                        SearchPredicateFactory theF,
1398                        BooleanPredicateClausesStep<?> theB,
1399                        ValueSet.ConceptSetFilterComponent theFilter) {
1400                if (isBlank(theFilter.getValue()) && theFilter.getOp() == null && isBlank(theFilter.getProperty())) {
1401                        return;
1402                }
1403
1404                // if filter type is EXISTS, there's no reason to worry about the value (we won't set it anyways)
1405                if ((isBlank(theFilter.getValue()) && theFilter.getOp() != ValueSet.FilterOperator.EXISTS)
1406                                || theFilter.getOp() == null
1407                                || isBlank(theFilter.getProperty())) {
1408                        throw new InvalidRequestException(
1409                                        Msg.code(891) + "Invalid filter, must have fields populated: property op value");
1410                }
1411
1412                switch (theFilter.getProperty()) {
1413                        case "display:exact":
1414                        case "display":
1415                                handleFilterDisplay(theF, theB, theFilter);
1416                                break;
1417                        case "concept":
1418                        case "code":
1419                                handleFilterConceptAndCode(theCodeSystemIdentifier, theF, theB, theFilter);
1420                                break;
1421                        case "parent":
1422                        case "child":
1423                                isCodeSystemLoincOrThrowInvalidRequestException(theCodeSystemIdentifier, theFilter.getProperty());
1424                                handleFilterLoincParentChild(theF, theB, theFilter);
1425                                break;
1426                        case "ancestor":
1427                                isCodeSystemLoincOrThrowInvalidRequestException(theCodeSystemIdentifier, theFilter.getProperty());
1428                                handleFilterLoincAncestor(theCodeSystemIdentifier, theF, theB, theFilter);
1429                                break;
1430                        case "descendant":
1431                                isCodeSystemLoincOrThrowInvalidRequestException(theCodeSystemIdentifier, theFilter.getProperty());
1432                                handleFilterLoincDescendant(theCodeSystemIdentifier, theF, theB, theFilter);
1433                                break;
1434                        case "copyright":
1435                                isCodeSystemLoincOrThrowInvalidRequestException(theCodeSystemIdentifier, theFilter.getProperty());
1436                                handleFilterLoincCopyright(theF, theB, theFilter);
1437                                break;
1438                        default:
1439                                if (theFilter.getOp() == ValueSet.FilterOperator.REGEX) {
1440                                        handleFilterRegex(theF, theB, theFilter);
1441                                } else {
1442                                        handleFilterPropertyDefault(theF, theB, theFilter);
1443                                }
1444                                break;
1445                }
1446        }
1447
1448        private void handleFilterPropertyDefault(
1449                        SearchPredicateFactory theF,
1450                        BooleanPredicateClausesStep<?> theB,
1451                        ValueSet.ConceptSetFilterComponent theFilter) {
1452
1453                String value = theFilter.getValue();
1454                if (theFilter.getOp() == ValueSet.FilterOperator.EXISTS) {
1455                        // EXISTS has no value and is thus handled differently
1456                        Term term = new Term(CONCEPT_PROPERTY_PREFIX_NAME + theFilter.getProperty());
1457                        theB.must(theF.exists().field(term.field()));
1458                } else {
1459                        Term term = new Term(CONCEPT_PROPERTY_PREFIX_NAME + theFilter.getProperty(), value);
1460                        switch (theFilter.getOp()) {
1461                                case EQUAL:
1462                                        theB.must(theF.match().field(term.field()).matching(term.text()));
1463                                        break;
1464                                case IN:
1465                                case NOTIN:
1466                                        boolean isNotFilter = theFilter.getOp() == ValueSet.FilterOperator.NOTIN;
1467                                        // IN and NOTIN expect comma separated lists
1468                                        String[] values = term.text().split(",");
1469                                        Set<String> valueSet = new HashSet<>(Arrays.asList(values));
1470                                        if (isNotFilter) {
1471                                                theB.filter(theF.not(theF.terms().field(term.field()).matchingAny(valueSet)));
1472                                        } else {
1473                                                theB.filter(theF.terms().field(term.field()).matchingAny(valueSet));
1474                                        }
1475                                        break;
1476                                case REGEX:
1477                                case EXISTS:
1478                                case NULL:
1479                                case ISA:
1480                                case ISNOTA:
1481                                case DESCENDENTOF:
1482                                case GENERALIZES:
1483                                default:
1484                                        /*
1485                                         * We do not need to handle REGEX, because that's handled in parent
1486                                         * We also don't handle EXISTS because that's a separate area (with different term).
1487                                         * We add a match-none filter because otherwise it matches everything (not desired).
1488                                         */
1489                                        ourLog.error(
1490                                                        "Unsupported property filter {}. This may affect expansion, but will not cause errors.",
1491                                                        theFilter.getOp().getDisplay());
1492                                        theB.must(theF.matchNone());
1493                                        break;
1494                        }
1495                }
1496        }
1497
1498        private void handleFilterRegex(
1499                        SearchPredicateFactory theF,
1500                        BooleanPredicateClausesStep<?> theB,
1501                        ValueSet.ConceptSetFilterComponent theFilter) {
1502                /*
1503                 * We treat the regex filter as a match on the regex
1504                 * anywhere in the property string. The spec does not
1505                 * say whether this is the right behaviour or not, but
1506                 * there are examples that seem to suggest that it is.
1507                 */
1508                String value = theFilter.getValue();
1509                if (value.endsWith("$")) {
1510                        value = value.substring(0, value.length() - 1);
1511                } else if (!value.endsWith(".*")) {
1512                        value = value + ".*";
1513                }
1514                if (!value.startsWith("^") && !value.startsWith(".*")) {
1515                        value = ".*" + value;
1516                } else if (value.startsWith("^")) {
1517                        value = value.substring(1);
1518                }
1519
1520                theB.must(theF.regexp()
1521                                .field(CONCEPT_PROPERTY_PREFIX_NAME + theFilter.getProperty())
1522                                .matching(value));
1523        }
1524
1525        private void handleFilterLoincCopyright(
1526                        SearchPredicateFactory theF,
1527                        BooleanPredicateClausesStep<?> theB,
1528                        ValueSet.ConceptSetFilterComponent theFilter) {
1529
1530                if (theFilter.getOp() == ValueSet.FilterOperator.EQUAL) {
1531
1532                        String copyrightFilterValue = defaultString(theFilter.getValue()).toLowerCase();
1533                        switch (copyrightFilterValue) {
1534                                case "3rdparty":
1535                                        logFilteringValueOnProperty(theFilter.getValue(), theFilter.getProperty());
1536                                        addFilterLoincCopyright3rdParty(theF, theB);
1537                                        break;
1538                                case "loinc":
1539                                        logFilteringValueOnProperty(theFilter.getValue(), theFilter.getProperty());
1540                                        addFilterLoincCopyrightLoinc(theF, theB);
1541                                        break;
1542                                default:
1543                                        throwInvalidRequestForValueOnProperty(theFilter.getValue(), theFilter.getProperty());
1544                        }
1545
1546                } else {
1547                        throwInvalidRequestForOpOnProperty(theFilter.getOp(), theFilter.getProperty());
1548                }
1549        }
1550
1551        private void addFilterLoincCopyrightLoinc(SearchPredicateFactory theF, BooleanPredicateClausesStep<?> theB) {
1552                theB.mustNot(theF.exists().field(CONCEPT_PROPERTY_PREFIX_NAME + "EXTERNAL_COPYRIGHT_NOTICE"));
1553        }
1554
1555        private void addFilterLoincCopyright3rdParty(SearchPredicateFactory theF, BooleanPredicateClausesStep<?> theB) {
1556                theB.must(theF.exists().field(CONCEPT_PROPERTY_PREFIX_NAME + "EXTERNAL_COPYRIGHT_NOTICE"));
1557        }
1558
1559        @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
1560        private void handleFilterLoincAncestor(
1561                        String theSystem,
1562                        SearchPredicateFactory f,
1563                        BooleanPredicateClausesStep<?> b,
1564                        ValueSet.ConceptSetFilterComponent theFilter) {
1565                switch (theFilter.getOp()) {
1566                        case EQUAL:
1567                                addLoincFilterAncestorEqual(theSystem, f, b, theFilter);
1568                                break;
1569                        case IN:
1570                                addLoincFilterAncestorIn(theSystem, f, b, theFilter);
1571                                break;
1572                        default:
1573                                throw new InvalidRequestException(Msg.code(892) + "Don't know how to handle op=" + theFilter.getOp()
1574                                                + " on property " + theFilter.getProperty());
1575                }
1576        }
1577
1578        private void addLoincFilterAncestorEqual(
1579                        String theSystem,
1580                        SearchPredicateFactory f,
1581                        BooleanPredicateClausesStep<?> b,
1582                        ValueSet.ConceptSetFilterComponent theFilter) {
1583                addLoincFilterAncestorEqual(theSystem, f, b, theFilter.getProperty(), theFilter.getValue());
1584        }
1585
1586        private void addLoincFilterAncestorEqual(
1587                        String theSystem,
1588                        SearchPredicateFactory f,
1589                        BooleanPredicateClausesStep<?> b,
1590                        String theProperty,
1591                        String theValue) {
1592                List<Term> terms = getAncestorTerms(theSystem, theProperty, theValue);
1593                b.must(f.bool(innerB -> terms.forEach(
1594                                term -> innerB.should(f.match().field(term.field()).matching(term.text())))));
1595        }
1596
1597        private void addLoincFilterAncestorIn(
1598                        String theSystem,
1599                        SearchPredicateFactory f,
1600                        BooleanPredicateClausesStep<?> b,
1601                        ValueSet.ConceptSetFilterComponent theFilter) {
1602                String[] values = theFilter.getValue().split(",");
1603                List<Term> terms = new ArrayList<>();
1604                for (String value : values) {
1605                        terms.addAll(getAncestorTerms(theSystem, theFilter.getProperty(), value));
1606                }
1607                b.must(f.bool(innerB -> terms.forEach(
1608                                term -> innerB.should(f.match().field(term.field()).matching(term.text())))));
1609        }
1610
1611        @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
1612        private void handleFilterLoincParentChild(
1613                        SearchPredicateFactory f, BooleanPredicateClausesStep<?> b, ValueSet.ConceptSetFilterComponent theFilter) {
1614                switch (theFilter.getOp()) {
1615                        case EQUAL:
1616                                addLoincFilterParentChildEqual(f, b, theFilter.getProperty(), theFilter.getValue());
1617                                break;
1618                        case IN:
1619                                addLoincFilterParentChildIn(f, b, theFilter);
1620                                break;
1621                        default:
1622                                throw new InvalidRequestException(Msg.code(893) + "Don't know how to handle op=" + theFilter.getOp()
1623                                                + " on property " + theFilter.getProperty());
1624                }
1625        }
1626
1627        private void addLoincFilterParentChildIn(
1628                        SearchPredicateFactory f, BooleanPredicateClausesStep<?> b, ValueSet.ConceptSetFilterComponent theFilter) {
1629                String[] values = theFilter.getValue().split(",");
1630                List<Term> terms = new ArrayList<>();
1631                for (String value : values) {
1632                        logFilteringValueOnProperty(value, theFilter.getProperty());
1633                        terms.add(getPropertyTerm(theFilter.getProperty(), value));
1634                }
1635
1636                b.must(f.bool(innerB -> terms.forEach(
1637                                term -> innerB.should(f.match().field(term.field()).matching(term.text())))));
1638        }
1639
1640        private void addLoincFilterParentChildEqual(
1641                        SearchPredicateFactory f, BooleanPredicateClausesStep<?> b, String theProperty, String theValue) {
1642                logFilteringValueOnProperty(theValue, theProperty);
1643                b.must(f.match().field(CONCEPT_PROPERTY_PREFIX_NAME + theProperty).matching(theValue));
1644        }
1645
1646        private void handleFilterConceptAndCode(
1647                        String theSystem,
1648                        SearchPredicateFactory f,
1649                        BooleanPredicateClausesStep<?> b,
1650                        ValueSet.ConceptSetFilterComponent theFilter) {
1651                TermConcept code = findCodeForFilterCriteriaCodeOrConcept(theSystem, theFilter);
1652
1653                if (theFilter.getOp() == ValueSet.FilterOperator.ISA) {
1654                        ourLog.debug(
1655                                        " * Filtering on specific code and codes with a parent of {}/{}/{}",
1656                                        code.getId(),
1657                                        code.getCode(),
1658                                        code.getDisplay());
1659
1660                        b.must(f.bool()
1661                                        .should(f.match().field("myParentPids").matching("" + code.getId()))
1662                                        .should(f.match().field("myId").matching(code.getPid())));
1663                } else if (theFilter.getOp() == ValueSet.FilterOperator.DESCENDENTOF) {
1664                        ourLog.debug(
1665                                        " * Filtering on codes with a parent of {}/{}/{}", code.getId(), code.getCode(), code.getDisplay());
1666
1667                        b.must(f.match().field("myParentPids").matching("" + code.getId()));
1668                } else {
1669                        throwInvalidFilter(theFilter, "");
1670                }
1671        }
1672
1673        @Nonnull
1674        private TermConcept findCodeForFilterCriteriaCodeOrConcept(
1675                        String theSystem, ValueSet.ConceptSetFilterComponent theFilter) {
1676                return findCode(theSystem, theFilter.getValue())
1677                                .orElseThrow(() ->
1678                                                new InvalidRequestException(Msg.code(2071) + "Invalid filter criteria - code does not exist: {"
1679                                                                + Constants.codeSystemWithDefaultDescription(theSystem) + "}" + theFilter.getValue()));
1680        }
1681
1682        private void throwInvalidFilter(ValueSet.ConceptSetFilterComponent theFilter, String theErrorSuffix) {
1683                throw new InvalidRequestException(Msg.code(894) + "Don't know how to handle op=" + theFilter.getOp()
1684                                + " on property " + theFilter.getProperty() + theErrorSuffix);
1685        }
1686
1687        private void isCodeSystemLoincOrThrowInvalidRequestException(String theSystemIdentifier, String theProperty) {
1688                String systemUrl = getUrlFromIdentifier(theSystemIdentifier);
1689                if (!isCodeSystemLoinc(systemUrl)) {
1690                        throw new InvalidRequestException(Msg.code(895) + "Invalid filter, property " + theProperty
1691                                        + " is LOINC-specific and cannot be used with system: " + systemUrl);
1692                }
1693        }
1694
1695        private boolean isCodeSystemLoinc(String theSystem) {
1696                return LOINC_URI.equals(theSystem);
1697        }
1698
1699        private void handleFilterDisplay(
1700                        SearchPredicateFactory f, BooleanPredicateClausesStep<?> b, ValueSet.ConceptSetFilterComponent theFilter) {
1701                if (theFilter.getProperty().equals("display:exact") && theFilter.getOp() == ValueSet.FilterOperator.EQUAL) {
1702                        addDisplayFilterExact(f, b, theFilter);
1703                } else if (theFilter.getProperty().equals("display") && theFilter.getOp() == ValueSet.FilterOperator.EQUAL) {
1704                        if (theFilter.getValue().trim().contains(" ")) {
1705                                addDisplayFilterExact(f, b, theFilter);
1706                        } else {
1707                                addDisplayFilterInexact(f, b, theFilter);
1708                        }
1709                }
1710        }
1711
1712        private void addDisplayFilterExact(
1713                        SearchPredicateFactory f,
1714                        BooleanPredicateClausesStep<?> bool,
1715                        ValueSet.ConceptSetFilterComponent nextFilter) {
1716                bool.must(f.phrase().field("myDisplay").matching(nextFilter.getValue()));
1717        }
1718
1719        private void addDisplayFilterInexact(
1720                        SearchPredicateFactory f,
1721                        BooleanPredicateClausesStep<?> bool,
1722                        ValueSet.ConceptSetFilterComponent nextFilter) {
1723                bool.must(f.phrase()
1724                                .field("myDisplay")
1725                                .boost(4.0f)
1726                                .field("myDisplayWordEdgeNGram")
1727                                .boost(1.0f)
1728                                .field("myDisplayEdgeNGram")
1729                                .boost(1.0f)
1730                                .matching(nextFilter.getValue().toLowerCase())
1731                                .slop(2));
1732        }
1733
1734        private Term getPropertyTerm(String theProperty, String theValue) {
1735                return new Term(CONCEPT_PROPERTY_PREFIX_NAME + theProperty, theValue);
1736        }
1737
1738        private List<Term> getAncestorTerms(String theSystem, String theProperty, String theValue) {
1739                List<Term> retVal = new ArrayList<>();
1740
1741                TermConcept code = findCode(theSystem, theValue)
1742                                .orElseThrow(() -> new InvalidRequestException("Invalid filter criteria - code does not exist: {"
1743                                                + Constants.codeSystemWithDefaultDescription(theSystem) + "}" + theValue));
1744
1745                retVal.add(new Term("myParentPids", "" + code.getId()));
1746                logFilteringValueOnProperty(theValue, theProperty);
1747
1748                return retVal;
1749        }
1750
1751        @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
1752        private void handleFilterLoincDescendant(
1753                        String theSystem,
1754                        SearchPredicateFactory f,
1755                        BooleanPredicateClausesStep<?> b,
1756                        ValueSet.ConceptSetFilterComponent theFilter) {
1757                switch (theFilter.getOp()) {
1758                        case EQUAL:
1759                                addLoincFilterDescendantEqual(theSystem, f, b, theFilter);
1760                                break;
1761                        case IN:
1762                                addLoincFilterDescendantIn(theSystem, f, b, theFilter);
1763                                break;
1764                        default:
1765                                throw new InvalidRequestException(Msg.code(896) + "Don't know how to handle op=" + theFilter.getOp()
1766                                                + " on property " + theFilter.getProperty());
1767                }
1768        }
1769
1770        private void addLoincFilterDescendantEqual(
1771                        String theSystem,
1772                        SearchPredicateFactory f,
1773                        BooleanPredicateClausesStep<?> b,
1774                        ValueSet.ConceptSetFilterComponent theFilter) {
1775
1776                List<TermConcept.TermConceptPk> parentPids =
1777                                getCodeParentPids(theSystem, theFilter.getProperty(), theFilter.getValue());
1778                if (parentPids.isEmpty()) {
1779                        // Can't return empty must, because it wil match according to other predicates.
1780                        // Some day there will be a 'matchNone' predicate
1781                        // (https://discourse.hibernate.org/t/fail-fast-predicate/6062)
1782                        b.mustNot(f.matchAll());
1783                        return;
1784                }
1785
1786                b.must(f.bool(innerB -> {
1787                        innerB.minimumShouldMatchNumber(1);
1788                        parentPids.forEach(pid -> innerB.should(f.match().field("myId").matching(pid)));
1789                }));
1790        }
1791
1792        /**
1793         * We are looking for codes which have codes indicated in theFilter.getValue() as descendants.
1794         * Strategy is to find codes which have their pId(s) in the list of the parentId(s) of all the TermConcept(s)
1795         * representing the codes in theFilter.getValue()
1796         */
1797        private void addLoincFilterDescendantIn(
1798                        String theSystem,
1799                        SearchPredicateFactory f,
1800                        BooleanPredicateClausesStep<?> b,
1801                        ValueSet.ConceptSetFilterComponent theFilter) {
1802
1803                String[] values = theFilter.getValue().split(",");
1804                if (values.length == 0) {
1805                        throw new InvalidRequestException(Msg.code(2062) + "Invalid filter criteria - no codes specified");
1806                }
1807
1808                List<TermConcept.TermConceptPk> descendantCodePidList =
1809                                getMultipleCodeParentPids(theSystem, theFilter.getProperty(), values);
1810
1811                b.must(f.bool(innerB -> descendantCodePidList.forEach(
1812                                pId -> innerB.should(f.match().field("myId").matching(pId)))));
1813        }
1814
1815        /**
1816         * Returns the list of parentId(s) of the TermConcept representing theValue as a code
1817         */
1818        private List<TermConcept.TermConceptPk> getCodeParentPids(String theSystem, String theProperty, String theValue) {
1819                TermConcept code = findCode(theSystem, theValue)
1820                                .orElseThrow(() -> new InvalidRequestException("Invalid filter criteria - code does not exist: {"
1821                                                + Constants.codeSystemWithDefaultDescription(theSystem) + "}" + theValue));
1822
1823                String[] parentPids = code.getParentPidsAsString().split(" ");
1824                List<TermConcept.TermConceptPk> retVal = Arrays.stream(parentPids)
1825                                .filter(pid -> !StringUtils.equals(pid, "NONE"))
1826                                .map(Long::parseLong)
1827                                .map(t -> new TermConcept.TermConceptPk(t, myPartitionSettings.getDefaultPartitionId()))
1828                                .collect(Collectors.toList());
1829                logFilteringValueOnProperty(theValue, theProperty);
1830                return retVal;
1831        }
1832
1833        /**
1834         * Returns the list of parentId(s) of the TermConcept representing theValue as a code
1835         */
1836        private List<TermConcept.TermConceptPk> getMultipleCodeParentPids(
1837                        String theSystem, String theProperty, String[] theValues) {
1838                List<String> valuesList = Arrays.asList(theValues);
1839                List<TermConcept> termConcepts = findCodes(theSystem, valuesList);
1840                if (valuesList.size() != termConcepts.size()) {
1841                        String exMsg = getTermConceptsFetchExceptionMsg(termConcepts, valuesList);
1842                        throw new InvalidRequestException(Msg.code(2064) + "Invalid filter criteria - {"
1843                                        + Constants.codeSystemWithDefaultDescription(theSystem) + "}: " + exMsg);
1844                }
1845
1846                List<TermConcept.TermConceptPk> retVal = termConcepts.stream()
1847                                .flatMap(tc -> Arrays.stream(tc.getParentPidsAsString().split(" ")))
1848                                .filter(pid -> !StringUtils.equals(pid, "NONE"))
1849                                .map(Long::parseLong)
1850                                .map(t -> new TermConcept.TermConceptPk(t, myPartitionSettings.getDefaultPartitionId()))
1851                                .collect(Collectors.toList());
1852
1853                logFilteringValueOnProperties(valuesList, theProperty);
1854
1855                return retVal;
1856        }
1857
1858        /**
1859         * Generate message indicating for which of theValues a TermConcept was not found
1860         */
1861        private String getTermConceptsFetchExceptionMsg(List<TermConcept> theTermConcepts, List<String> theValues) {
1862                // case: more TermConcept(s) retrieved than codes queried
1863                if (theTermConcepts.size() > theValues.size()) {
1864                        return "Invalid filter criteria - More TermConcepts were found than indicated codes. Queried codes: ["
1865                                        + join(
1866                                                        ",",
1867                                                        theValues + "]; Obtained TermConcept IDs, codes: ["
1868                                                                        + theTermConcepts.stream()
1869                                                                                        .map(tc -> tc.getId() + ", " + tc.getCode())
1870                                                                                        .collect(joining("; "))
1871                                                                        + "]");
1872                }
1873
1874                // case: less TermConcept(s) retrieved than codes queried
1875                Set<String> matchedCodes =
1876                                theTermConcepts.stream().map(TermConcept::getCode).collect(toSet());
1877                List<String> notMatchedValues =
1878                                theValues.stream().filter(v -> !matchedCodes.contains(v)).collect(toList());
1879
1880                return "Invalid filter criteria - No TermConcept(s) were found for the requested codes: ["
1881                                + join(",", notMatchedValues + "]");
1882        }
1883
1884        private void logFilteringValueOnProperty(String theValue, String theProperty) {
1885                ourLog.debug(" * Filtering with value={} on property {}", theValue, theProperty);
1886        }
1887
1888        private void logFilteringValueOnProperties(List<String> theValues, String theProperty) {
1889                ourLog.debug(" * Filtering with values={} on property {}", String.join(", ", theValues), theProperty);
1890        }
1891
1892        private void throwInvalidRequestForOpOnProperty(ValueSet.FilterOperator theOp, String theProperty) {
1893                throw new InvalidRequestException(
1894                                Msg.code(897) + "Don't know how to handle op=" + theOp + " on property " + theProperty);
1895        }
1896
1897        private void throwInvalidRequestForValueOnProperty(String theValue, String theProperty) {
1898                throw new InvalidRequestException(
1899                                Msg.code(898) + "Don't know how to handle value=" + theValue + " on property " + theProperty);
1900        }
1901
1902        private void expandWithoutHibernateSearch(
1903                        IValueSetConceptAccumulator theValueSetCodeAccumulator,
1904                        TermCodeSystemVersion theVersion,
1905                        Set<String> theAddedCodes,
1906                        ValueSet.ConceptSetComponent theInclude,
1907                        String theSystem,
1908                        boolean theAdd) {
1909                ourLog.trace("Hibernate search is not enabled");
1910
1911                if (theValueSetCodeAccumulator instanceof ValueSetExpansionComponentWithConceptAccumulator) {
1912                        Validate.isTrue(
1913                                        ((ValueSetExpansionComponentWithConceptAccumulator) theValueSetCodeAccumulator)
1914                                                        .getParameter()
1915                                                        .isEmpty(),
1916                                        "Can not expand ValueSet with parameters - Hibernate Search is not enabled on this server.");
1917                }
1918
1919                Validate.isTrue(
1920                                isNotBlank(theSystem),
1921                                "Can not expand ValueSet without explicit system - Hibernate Search is not enabled on this server.");
1922
1923                for (ValueSet.ConceptSetFilterComponent nextFilter : theInclude.getFilter()) {
1924                        boolean handled = false;
1925                        switch (nextFilter.getProperty().toLowerCase()) {
1926                                case "concept":
1927                                case "code":
1928                                        if (nextFilter.getOp() == ValueSet.FilterOperator.ISA) {
1929                                                theValueSetCodeAccumulator.addMessage(
1930                                                                "Processing IS-A filter in database - Note that Hibernate Search is not enabled on this server, so this operation can be inefficient.");
1931                                                TermConcept code = findCodeForFilterCriteriaCodeOrConcept(theSystem, nextFilter);
1932                                                addConceptAndChildren(
1933                                                                theValueSetCodeAccumulator, theAddedCodes, theInclude, theSystem, theAdd, code);
1934                                                handled = true;
1935                                        }
1936                                        break;
1937                                default:
1938                                        // TODO - we need to handle other properties (fields)
1939                                        // and other operations (not just is-a)
1940                                        // in some (preferably generic) way
1941                                        break;
1942                        }
1943
1944                        if (!handled) {
1945                                throwInvalidFilter(
1946                                                nextFilter,
1947                                                " - Note that Hibernate Search is disabled on this server so not all ValueSet expansion functionality is available.");
1948                        }
1949                }
1950
1951                if (theInclude.getFilter().isEmpty() && theInclude.getConcept().isEmpty()) {
1952                        Collection<TermConcept> concepts =
1953                                        myConceptDao.fetchConceptsAndDesignationsByVersionPid(theVersion.getPid());
1954                        for (TermConcept next : concepts) {
1955                                addCodeIfNotAlreadyAdded(
1956                                                theValueSetCodeAccumulator,
1957                                                theAddedCodes,
1958                                                theAdd,
1959                                                theSystem,
1960                                                theInclude.getVersion(),
1961                                                next.getCode(),
1962                                                next.getDisplay(),
1963                                                next.getId(),
1964                                                next.getParentPidsAsString(),
1965                                                next.getDesignations());
1966                        }
1967                }
1968
1969                for (ValueSet.ConceptReferenceComponent next : theInclude.getConcept()) {
1970                        if (!theSystem.equals(theInclude.getSystem()) && isNotBlank(theSystem)) {
1971                                continue;
1972                        }
1973                        Collection<TermConceptDesignation> designations = next.getDesignation().stream()
1974                                        .map(t -> new TermConceptDesignation()
1975                                                        .setValue(t.getValue())
1976                                                        .setLanguage(t.getLanguage())
1977                                                        .setUseCode(t.getUse().getCode())
1978                                                        .setUseSystem(t.getUse().getSystem())
1979                                                        .setUseDisplay(t.getUse().getDisplay()))
1980                                        .collect(Collectors.toList());
1981                        addCodeIfNotAlreadyAdded(
1982                                        theValueSetCodeAccumulator,
1983                                        theAddedCodes,
1984                                        theAdd,
1985                                        theSystem,
1986                                        theInclude.getVersion(),
1987                                        next.getCode(),
1988                                        next.getDisplay(),
1989                                        null,
1990                                        null,
1991                                        designations);
1992                }
1993        }
1994
1995        private void addConceptAndChildren(
1996                        IValueSetConceptAccumulator theValueSetCodeAccumulator,
1997                        Set<String> theAddedCodes,
1998                        ValueSet.ConceptSetComponent theInclude,
1999                        String theSystem,
2000                        boolean theAdd,
2001                        TermConcept theConcept) {
2002                for (TermConcept nextChild : theConcept.getChildCodes()) {
2003                        boolean added = addCodeIfNotAlreadyAdded(
2004                                        theValueSetCodeAccumulator,
2005                                        theAddedCodes,
2006                                        theAdd,
2007                                        theSystem,
2008                                        theInclude.getVersion(),
2009                                        nextChild.getCode(),
2010                                        nextChild.getDisplay(),
2011                                        nextChild.getId(),
2012                                        nextChild.getParentPidsAsString(),
2013                                        nextChild.getDesignations());
2014                        if (added) {
2015                                addConceptAndChildren(
2016                                                theValueSetCodeAccumulator, theAddedCodes, theInclude, theSystem, theAdd, nextChild);
2017                        }
2018                }
2019        }
2020
2021        @Override
2022        @Transactional
2023        public String invalidatePreCalculatedExpansion(IIdType theValueSetId, RequestDetails theRequestDetails) {
2024                IBaseResource valueSet = myDaoRegistry.getResourceDao("ValueSet").read(theValueSetId, theRequestDetails);
2025                ValueSet canonicalValueSet = myVersionCanonicalizer.valueSetToCanonical(valueSet);
2026                Optional<TermValueSet> optionalTermValueSet = fetchValueSetEntity(canonicalValueSet);
2027                if (optionalTermValueSet.isEmpty()) {
2028                        return myContext
2029                                        .getLocalizer()
2030                                        .getMessage(TermReadSvcImpl.class, "valueSetNotFoundInTerminologyDatabase", theValueSetId);
2031                }
2032
2033                ourLog.info(
2034                                "Invalidating pre-calculated expansion on ValueSet {} / {}", theValueSetId, canonicalValueSet.getUrl());
2035
2036                TermValueSet termValueSet = optionalTermValueSet.get();
2037                if (termValueSet.getExpansionStatus() == TermValueSetPreExpansionStatusEnum.NOT_EXPANDED) {
2038                        return myContext
2039                                        .getLocalizer()
2040                                        .getMessage(
2041                                                        TermReadSvcImpl.class,
2042                                                        "valueSetCantInvalidateNotYetPrecalculated",
2043                                                        termValueSet.getUrl(),
2044                                                        termValueSet.getExpansionStatus());
2045                }
2046
2047                Long totalConcepts = termValueSet.getTotalConcepts();
2048
2049                deletePreCalculatedValueSetContents(termValueSet);
2050
2051                termValueSet.setExpansionStatus(TermValueSetPreExpansionStatusEnum.NOT_EXPANDED);
2052                termValueSet.setExpansionTimestamp(null);
2053
2054                assert termValueSet.getId() != null;
2055                myEntityManager.merge(termValueSet);
2056
2057                afterValueSetExpansionStatusChange();
2058
2059                return myContext
2060                                .getLocalizer()
2061                                .getMessage(
2062                                                TermReadSvcImpl.class, "valueSetPreExpansionInvalidated", termValueSet.getUrl(), totalConcepts);
2063        }
2064
2065        @Override
2066        @Transactional(readOnly = true)
2067        public boolean isValueSetPreExpandedForCodeValidation(ValueSet theValueSet) {
2068                Optional<TermValueSet> optionalTermValueSet = fetchValueSetEntity(theValueSet);
2069
2070                if (optionalTermValueSet.isEmpty()) {
2071                        ourLog.warn(
2072                                        "ValueSet is not present in terminology tables. Will perform in-memory code validation. {}",
2073                                        getValueSetInfo(theValueSet));
2074                        return false;
2075                }
2076
2077                TermValueSet termValueSet = optionalTermValueSet.get();
2078
2079                if (termValueSet.getExpansionStatus() != TermValueSetPreExpansionStatusEnum.EXPANDED) {
2080                        ourLog.warn(
2081                                        "{} is present in terminology tables but not ready for persistence-backed invocation of operation $validation-code. Will perform in-memory code validation. Current status: {} | {}",
2082                                        getValueSetInfo(theValueSet),
2083                                        termValueSet.getExpansionStatus().name(),
2084                                        termValueSet.getExpansionStatus().getDescription());
2085                        return false;
2086                }
2087
2088                return true;
2089        }
2090
2091        @SuppressWarnings({"OptionalAssignedToNull", "unchecked"})
2092        private Optional<TermValueSet> fetchValueSetEntity(ValueSet theValueSet) {
2093                Optional<TermValueSet> retVal = (Optional<TermValueSet>) theValueSet.getUserData(VS_USERDATA_CURRENT_VERSION);
2094                if (retVal == null) {
2095                        synchronized (theValueSet) {
2096                                JpaPid valueSetResourcePid = getValueSetResourcePersistentId(theValueSet);
2097                                retVal = myTermValueSetDao.findByResourcePid(valueSetResourcePid);
2098                                theValueSet.setUserData(VS_USERDATA_CURRENT_VERSION, retVal);
2099                        }
2100                }
2101
2102                return retVal;
2103        }
2104
2105        private JpaPid getValueSetResourcePersistentId(ValueSet theValueSet) {
2106                return myIdHelperService.resolveResourceIdentityPid(
2107                                RequestPartitionId.allPartitions(),
2108                                theValueSet.getIdElement().getResourceType(),
2109                                theValueSet.getIdElement().getIdPart(),
2110                                ResolveIdentityMode.includeDeleted().cacheOk());
2111        }
2112
2113        protected IValidationSupport.CodeValidationResult validateCodeIsInPreExpandedValueSet(
2114                        ValidationSupportContext theValidationSupportContext,
2115                        ConceptValidationOptions theValidationOptions,
2116                        ValueSet theValueSet,
2117                        String theSystem,
2118                        String theCode,
2119                        String theDisplay,
2120                        Coding theCoding,
2121                        CodeableConcept theCodeableConcept) {
2122                assert TransactionSynchronizationManager.isSynchronizationActive();
2123
2124                ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSet.hasId(), "ValueSet.id is required");
2125                JpaPid valueSetResourcePid = getValueSetResourcePersistentId(theValueSet);
2126
2127                List<TermValueSetConcept> concepts = new ArrayList<>();
2128                if (isNotBlank(theCode)) {
2129                        if (theValidationOptions.isInferSystem()) {
2130                                concepts.addAll(
2131                                                myValueSetConceptDao.findByValueSetResourcePidAndCode(valueSetResourcePid.getId(), theCode));
2132                        } else if (isNotBlank(theSystem)) {
2133                                concepts.addAll(findByValueSetResourcePidSystemAndCode(valueSetResourcePid, theSystem, theCode));
2134                        }
2135                } else if (theCoding != null) {
2136                        if (theCoding.hasSystem() && theCoding.hasCode()) {
2137                                concepts.addAll(findByValueSetResourcePidSystemAndCode(
2138                                                valueSetResourcePid, theCoding.getSystem(), theCoding.getCode()));
2139                        }
2140                } else if (theCodeableConcept != null) {
2141                        for (Coding coding : theCodeableConcept.getCoding()) {
2142                                if (coding.hasSystem() && coding.hasCode()) {
2143                                        concepts.addAll(findByValueSetResourcePidSystemAndCode(
2144                                                        valueSetResourcePid, coding.getSystem(), coding.getCode()));
2145                                        if (!concepts.isEmpty()) {
2146                                                break;
2147                                        }
2148                                }
2149                        }
2150                } else {
2151                        return null;
2152                }
2153
2154                TermValueSet valueSetEntity = fetchValueSetEntity(theValueSet).orElseThrow(IllegalStateException::new);
2155                String timingDescription = toHumanReadableExpansionTimestamp(valueSetEntity);
2156                String preExpansionMessage = myContext
2157                                .getLocalizer()
2158                                .getMessage(TermReadSvcImpl.class, "validationPerformedAgainstPreExpansion", timingDescription);
2159
2160                if (theValidationOptions.isValidateDisplay() && concepts.size() > 0) {
2161                        String systemVersion = null;
2162                        for (TermValueSetConcept concept : concepts) {
2163                                systemVersion = concept.getSystemVersion();
2164                                if (isBlank(theDisplay) || isBlank(concept.getDisplay()) || theDisplay.equals(concept.getDisplay())) {
2165                                        return new IValidationSupport.CodeValidationResult()
2166                                                        .setCode(concept.getCode())
2167                                                        .setDisplay(concept.getDisplay())
2168                                                        .setCodeSystemVersion(concept.getSystemVersion())
2169                                                        .setSourceDetails(preExpansionMessage);
2170                                }
2171                        }
2172
2173                        String expectedDisplay = concepts.get(0).getDisplay();
2174                        return InMemoryTerminologyServerValidationSupport.createResultForDisplayMismatch(
2175                                        myContext,
2176                                        theCode,
2177                                        theDisplay,
2178                                        expectedDisplay,
2179                                        theSystem,
2180                                        systemVersion,
2181                                        myStorageSettings.getIssueSeverityForCodeDisplayMismatch());
2182                }
2183
2184                if (!concepts.isEmpty()) {
2185                        return new IValidationSupport.CodeValidationResult()
2186                                        .setCode(concepts.get(0).getCode())
2187                                        .setDisplay(concepts.get(0).getDisplay())
2188                                        .setCodeSystemVersion(concepts.get(0).getSystemVersion())
2189                                        .setMessage(preExpansionMessage);
2190                }
2191
2192                // Ok, we failed
2193                List<TermValueSetConcept> outcome = myValueSetConceptDao.findByTermValueSetIdSystemOnly(
2194                                Pageable.ofSize(1), valueSetEntity.getId(), theSystem);
2195                String append;
2196                if (outcome.size() == 0) {
2197                        append = " - No codes in ValueSet belong to CodeSystem with URL " + theSystem;
2198                } else {
2199                        String unknownCodeMessage = myContext
2200                                        .getLocalizer()
2201                                        .getMessage(TermReadSvcImpl.class, "unknownCodeInSystem", theSystem, theCode);
2202                        append = " - " + unknownCodeMessage + ". " + preExpansionMessage;
2203                }
2204
2205                return createCodeNotFoundErrorForValidationResult(theSystem, theCode, null, append);
2206        }
2207
2208        private CodeValidationResult createCodeNotFoundErrorForValidationResult(
2209                        String theSystem, String theCode, String theCodeSystemVersion, String theAppend) {
2210                String theMessage = "Unable to validate code " + theSystem + "#" + theCode + theAppend;
2211                // The InstanceValidator (core) will change the severity based on the binding strength
2212                return new CodeValidationResult()
2213                                .setSeverity(IssueSeverity.ERROR)
2214                                .setCodeSystemVersion(theCodeSystemVersion)
2215                                .setMessage(theMessage)
2216                                .addIssue(new CodeValidationIssue(
2217                                                theMessage,
2218                                                IssueSeverity.ERROR,
2219                                                CodeValidationIssueCode.NOT_FOUND,
2220                                                CodeValidationIssueCoding.NOT_FOUND));
2221        }
2222
2223        private List<TermValueSetConcept> findByValueSetResourcePidSystemAndCode(
2224                        JpaPid theResourcePid, String theSystem, String theCode) {
2225                assert TransactionSynchronizationManager.isSynchronizationActive();
2226
2227                List<TermValueSetConcept> retVal = new ArrayList<>();
2228                Optional<TermValueSetConcept> optionalTermValueSetConcept;
2229                int versionIndex = theSystem.indexOf(OUR_PIPE_CHARACTER);
2230                if (versionIndex >= 0) {
2231                        String systemUrl = theSystem.substring(0, versionIndex);
2232                        String systemVersion = theSystem.substring(versionIndex + 1);
2233                        optionalTermValueSetConcept = myValueSetConceptDao.findByValueSetResourcePidSystemAndCodeWithVersion(
2234                                        theResourcePid.getId(), systemUrl, systemVersion, theCode);
2235                } else {
2236                        optionalTermValueSetConcept = myValueSetConceptDao.findByValueSetResourcePidSystemAndCode(
2237                                        theResourcePid.getId(), theSystem, theCode);
2238                }
2239                optionalTermValueSetConcept.ifPresent(retVal::add);
2240                return retVal;
2241        }
2242
2243        private void fetchChildren(TermConcept theConcept, Set<TermConcept> theSetToPopulate) {
2244                for (TermConceptParentChildLink nextChildLink : theConcept.getChildren()) {
2245                        TermConcept nextChild = nextChildLink.getChild();
2246                        if (addToSet(theSetToPopulate, nextChild)) {
2247                                fetchChildren(nextChild, theSetToPopulate);
2248                        }
2249                }
2250        }
2251
2252        private Optional<TermConcept> fetchLoadedCode(Long theCodeSystemResourcePid, String theCode) {
2253                TermCodeSystemVersion codeSystem =
2254                                myCodeSystemVersionDao.findCurrentVersionForCodeSystemResourcePid(theCodeSystemResourcePid);
2255                return myConceptDao.findByCodeSystemAndCode(codeSystem.getPid(), theCode);
2256        }
2257
2258        private void fetchParents(TermConcept theConcept, Set<TermConcept> theSetToPopulate) {
2259                for (TermConceptParentChildLink nextChildLink : theConcept.getParents()) {
2260                        TermConcept nextChild = nextChildLink.getParent();
2261                        if (addToSet(theSetToPopulate, nextChild)) {
2262                                fetchParents(nextChild, theSetToPopulate);
2263                        }
2264                }
2265        }
2266
2267        @Override
2268        public Optional<TermConcept> findCode(String theCodeSystem, String theCode) {
2269                /*
2270                 * Loading concepts without a transaction causes issues later on some
2271                 * platforms (e.g. PSQL) so make sure that we always call this with an open transaction
2272                 */
2273                HapiTransactionService.requireTransaction();
2274
2275                TermCodeSystemVersionDetails csv =
2276                                getCurrentCodeSystemVersion(new ValidationSupportContext(provideValidationSupport()), theCodeSystem);
2277                if (csv == null) {
2278                        return Optional.empty();
2279                }
2280                return myConceptDao.findByCodeSystemAndCode(csv.myPid, theCode);
2281        }
2282
2283        @Override
2284        public List<TermConcept> findCodes(String theCodeSystem, List<String> theCodeList) {
2285                HapiTransactionService.requireTransaction();
2286
2287                TermCodeSystemVersionDetails csv =
2288                                getCurrentCodeSystemVersion(new ValidationSupportContext(provideValidationSupport()), theCodeSystem);
2289                if (csv == null) {
2290                        return Collections.emptyList();
2291                }
2292
2293                return myConceptDao.findByCodeSystemAndCodeList(csv.myPid, theCodeList);
2294        }
2295
2296        @Nullable
2297        private TermCodeSystemVersionDetails getCurrentCodeSystemVersion(
2298                        ValidationSupportContext theValidationSupportContext, String theCodeSystemIdentifier) {
2299                String version = getVersionFromIdentifier(theCodeSystemIdentifier);
2300
2301                // Fetch the CodeSystem from ValidationSupport, which should return a cached copy. We
2302                // keep a copy of the current version entity in userData in that cached copy
2303                // to avoid repeated lookups
2304                TermCodeSystemVersionDetails retVal;
2305                IBaseResource codeSystem =
2306                                theValidationSupportContext.getRootValidationSupport().fetchCodeSystem(theCodeSystemIdentifier);
2307                if (codeSystem != null) {
2308
2309                        synchronized (codeSystem) {
2310                                retVal = (TermCodeSystemVersionDetails) codeSystem.getUserData(CS_USERDATA_CURRENT_VERSION);
2311                                if (retVal == null) {
2312                                        retVal = getCurrentCodeSystemVersion(theCodeSystemIdentifier, version);
2313                                        codeSystem.setUserData(CS_USERDATA_CURRENT_VERSION, retVal);
2314                                }
2315                        }
2316                } else {
2317                        retVal = getCurrentCodeSystemVersion(theCodeSystemIdentifier, version);
2318                }
2319
2320                return retVal;
2321        }
2322
2323        @Nullable
2324        private TermCodeSystemVersionDetails getCurrentCodeSystemVersion(String theCodeSystemIdentifier, String version) {
2325                TermCodeSystemVersionDetails retVal;
2326                retVal = myTxTemplate.execute(tx -> {
2327                        TermCodeSystemVersion csv = null;
2328                        TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(getUrlFromIdentifier(theCodeSystemIdentifier));
2329                        if (cs != null) {
2330                                if (version != null) {
2331                                        csv = myCodeSystemVersionDao.findByCodeSystemPidAndVersion(cs.getPid(), version);
2332                                } else if (cs.getCurrentVersion() != null) {
2333                                        csv = cs.getCurrentVersion();
2334                                }
2335                        }
2336                        if (csv != null) {
2337                                return new TermCodeSystemVersionDetails(csv.getPid(), csv.getCodeSystemVersionId());
2338                        } else {
2339                                return null;
2340                        }
2341                });
2342                return retVal;
2343        }
2344
2345        private String getVersionFromIdentifier(String theUri) {
2346                String retVal = null;
2347                if (StringUtils.isNotEmpty((theUri))) {
2348                        int versionSeparator = theUri.lastIndexOf('|');
2349                        if (versionSeparator != -1) {
2350                                retVal = theUri.substring(versionSeparator + 1);
2351                        }
2352                }
2353                return retVal;
2354        }
2355
2356        private String getUrlFromIdentifier(String theUri) {
2357                String retVal = theUri;
2358                if (StringUtils.isNotEmpty((theUri))) {
2359                        int versionSeparator = theUri.lastIndexOf('|');
2360                        if (versionSeparator != -1) {
2361                                retVal = theUri.substring(0, versionSeparator);
2362                        }
2363                }
2364                return retVal;
2365        }
2366
2367        @Transactional(propagation = Propagation.REQUIRED, readOnly = true)
2368        @Override
2369        public Set<TermConcept> findCodesAbove(
2370                        Long theCodeSystemResourcePid, Long theCodeSystemVersionPid, String theCode) {
2371                StopWatch stopwatch = new StopWatch();
2372
2373                Optional<TermConcept> concept = fetchLoadedCode(theCodeSystemResourcePid, theCode);
2374                if (concept.isEmpty()) {
2375                        return Collections.emptySet();
2376                }
2377
2378                Set<TermConcept> retVal = new HashSet<>();
2379                retVal.add(concept.get());
2380
2381                fetchParents(concept.get(), retVal);
2382
2383                ourLog.debug("Fetched {} codes above code {} in {}ms", retVal.size(), theCode, stopwatch.getMillis());
2384                return retVal;
2385        }
2386
2387        @Transactional(readOnly = true)
2388        @Override
2389        public List<FhirVersionIndependentConcept> findCodesAbove(String theSystem, String theCode) {
2390                TermCodeSystem cs = getCodeSystem(theSystem);
2391                if (cs == null) {
2392                        return findCodesAboveUsingBuiltInSystems(theSystem, theCode);
2393                }
2394                TermCodeSystemVersion csv = cs.getCurrentVersion();
2395
2396                Set<TermConcept> codes = findCodesAbove(cs.getResource().getId().getId(), csv.getPid(), theCode);
2397                return toVersionIndependentConcepts(theSystem, codes);
2398        }
2399
2400        @Transactional(propagation = Propagation.REQUIRED, readOnly = true)
2401        @Override
2402        public Set<TermConcept> findCodesBelow(
2403                        Long theCodeSystemResourcePid, Long theCodeSystemVersionPid, String theCode) {
2404                Stopwatch stopwatch = Stopwatch.createStarted();
2405
2406                Optional<TermConcept> concept = fetchLoadedCode(theCodeSystemResourcePid, theCode);
2407                if (concept.isEmpty()) {
2408                        return Collections.emptySet();
2409                }
2410
2411                Set<TermConcept> retVal = new HashSet<>();
2412                retVal.add(concept.get());
2413
2414                fetchChildren(concept.get(), retVal);
2415
2416                ourLog.debug(
2417                                "Fetched {} codes below code {} in {}ms",
2418                                retVal.size(),
2419                                theCode,
2420                                stopwatch.elapsed(TimeUnit.MILLISECONDS));
2421                return retVal;
2422        }
2423
2424        @Transactional(readOnly = true)
2425        @Override
2426        public List<FhirVersionIndependentConcept> findCodesBelow(String theSystem, String theCode) {
2427                TermCodeSystem cs = getCodeSystem(theSystem);
2428                if (cs == null) {
2429                        return findCodesBelowUsingBuiltInSystems(theSystem, theCode);
2430                }
2431                TermCodeSystemVersion csv = cs.getCurrentVersion();
2432
2433                Set<TermConcept> codes = findCodesBelow(cs.getResource().getId().getId(), csv.getPid(), theCode);
2434                return toVersionIndependentConcepts(theSystem, codes);
2435        }
2436
2437        private TermCodeSystem getCodeSystem(String theSystem) {
2438                return myCodeSystemDao.findByCodeSystemUri(theSystem);
2439        }
2440
2441        @PostConstruct
2442        public void start() {
2443                RuleBasedTransactionAttribute rules = new RuleBasedTransactionAttribute();
2444                rules.getRollbackRules().add(new NoRollbackRuleAttribute(ExpansionTooCostlyException.class));
2445                myTxTemplate = new TransactionTemplate(myTransactionManager, rules);
2446        }
2447
2448        @Override
2449        public void scheduleJobs(ISchedulerService theSchedulerService) {
2450                // Register scheduled job to pre-expand ValueSets
2451                // In the future it would be great to make this a cluster-aware task somehow
2452                ScheduledJobDefinition vsJobDefinition = new ScheduledJobDefinition();
2453                vsJobDefinition.setId(getClass().getName());
2454                vsJobDefinition.setJobClass(Job.class);
2455                theSchedulerService.scheduleClusteredJob(10 * DateUtils.MILLIS_PER_MINUTE, vsJobDefinition);
2456        }
2457
2458        @Override
2459        public synchronized void preExpandDeferredValueSetsToTerminologyTables() {
2460                if (!myStorageSettings.isEnableTaskPreExpandValueSets()) {
2461                        return;
2462                }
2463                if (isNotSafeToPreExpandValueSets()) {
2464                        ourLog.info("Skipping scheduled pre-expansion of ValueSets while deferred entities are being loaded.");
2465                        return;
2466                }
2467                TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
2468
2469                while (true) {
2470                        StopWatch sw = new StopWatch();
2471                        TermValueSet valueSetToExpand = txTemplate.execute(t -> {
2472                                Optional<TermValueSet> optionalTermValueSet = getNextTermValueSetNotExpanded();
2473                                if (optionalTermValueSet.isEmpty()) {
2474                                        return null;
2475                                }
2476
2477                                TermValueSet termValueSet = optionalTermValueSet.get();
2478                                termValueSet.setTotalConcepts(0L);
2479                                termValueSet.setTotalConceptDesignations(0L);
2480                                termValueSet.setExpansionStatus(TermValueSetPreExpansionStatusEnum.EXPANSION_IN_PROGRESS);
2481                                TermValueSet retVal = myEntityManager.merge(termValueSet);
2482                                myEntityManager.flush();
2483                                return retVal;
2484                        });
2485                        if (valueSetToExpand == null) {
2486                                return;
2487                        }
2488
2489                        // We have a ValueSet to pre-expand.
2490                        setPreExpandingValueSets(true);
2491                        try {
2492                                ValueSet valueSet = txTemplate.execute(t -> {
2493                                        TermValueSet refreshedValueSetToExpand = myTermValueSetDao
2494                                                        .findById(valueSetToExpand.getPartitionedId())
2495                                                        .orElseThrow(() -> new IllegalStateException("Unknown VS ID: " + valueSetToExpand.getId()));
2496                                        return getValueSetFromResourceTable(refreshedValueSetToExpand.getResource());
2497                                });
2498                                assert valueSet != null;
2499
2500                                ValueSetConceptAccumulator valueSetConceptAccumulator =
2501                                                myValueSetConceptAccumulatorFactory.create(valueSetToExpand);
2502                                ValueSetExpansionOptions options = new ValueSetExpansionOptions();
2503                                options.setIncludeHierarchy(true);
2504                                expandValueSet(options, valueSet, valueSetConceptAccumulator);
2505
2506                                // We are done with this ValueSet.
2507                                txTemplate.executeWithoutResult(t -> {
2508                                        valueSetToExpand.setExpansionStatus(TermValueSetPreExpansionStatusEnum.EXPANDED);
2509                                        valueSetToExpand.setExpansionTimestamp(new Date());
2510                                        myEntityManager.merge(valueSetToExpand);
2511                                });
2512
2513                                afterValueSetExpansionStatusChange();
2514
2515                                ourLog.info(
2516                                                "Pre-expanded ValueSet[{}] with URL[{}] - Saved {} concepts in {}",
2517                                                valueSet.getId(),
2518                                                valueSet.getUrl(),
2519                                                valueSetConceptAccumulator.getConceptsSaved(),
2520                                                sw);
2521
2522                        } catch (Exception e) {
2523                                ourLog.error(
2524                                                "Failed to pre-expand ValueSet with URL[{}]: {}", valueSetToExpand.getUrl(), e.getMessage(), e);
2525                                txTemplate.executeWithoutResult(t -> {
2526                                        valueSetToExpand.setExpansionStatus(TermValueSetPreExpansionStatusEnum.FAILED_TO_EXPAND);
2527                                        myEntityManager.merge(valueSetToExpand);
2528                                });
2529
2530                        } finally {
2531                                setPreExpandingValueSets(false);
2532                        }
2533                }
2534        }
2535
2536        /*
2537         * If a ValueSet has just finished pre-expanding, let's flush the caches. This is
2538         * kind of a blunt tool, but it should ensure that users don't get unpredictable
2539         * results while they test changes, which is probably a worthwhile sacrifice
2540         */
2541        private void afterValueSetExpansionStatusChange() {
2542                provideValidationSupport().invalidateCaches();
2543        }
2544
2545        @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2546        @Override
2547        public void invalidateCaches() {
2548                /*
2549                 * Clear out anything left in the userdata caches. We do this mostly because it messes
2550                 * up unit tests to have these things stick around between test runs, since many of
2551                 * these resources come from DefaultProfileValidationSupport and therefore live beyond
2552                 * any single test execution.
2553                 */
2554                for (IBaseResource next : provideValidationSupport().fetchAllConformanceResources()) {
2555                        if (next != null) {
2556                                synchronized (next) {
2557                                        if (next.getUserData(CS_USERDATA_CURRENT_VERSION) != null) {
2558                                                next.setUserData(CS_USERDATA_CURRENT_VERSION, null);
2559                                        }
2560                                        if (next.getUserData(VS_USERDATA_CURRENT_VERSION) != null) {
2561                                                next.setUserData(VS_USERDATA_CURRENT_VERSION, null);
2562                                        }
2563                                }
2564                        }
2565                }
2566        }
2567
2568        private synchronized boolean isPreExpandingValueSets() {
2569                return myPreExpandingValueSets;
2570        }
2571
2572        private synchronized void setPreExpandingValueSets(boolean thePreExpandingValueSets) {
2573                myPreExpandingValueSets = thePreExpandingValueSets;
2574        }
2575
2576        private boolean isNotSafeToPreExpandValueSets() {
2577                return myDeferredStorageSvc != null && !myDeferredStorageSvc.isStorageQueueEmpty(true);
2578        }
2579
2580        private Optional<TermValueSet> getNextTermValueSetNotExpanded() {
2581                Optional<TermValueSet> retVal = Optional.empty();
2582                Slice<TermValueSet> page = myTermValueSetDao.findByExpansionStatus(
2583                                PageRequest.of(0, 1), TermValueSetPreExpansionStatusEnum.NOT_EXPANDED);
2584
2585                if (!page.getContent().isEmpty()) {
2586                        retVal = Optional.of(page.getContent().get(0));
2587                }
2588
2589                return retVal;
2590        }
2591
2592        @Override
2593        @Transactional
2594        public void storeTermValueSet(ResourceTable theResourceTable, ValueSet theValueSet) {
2595                // If we're in a transaction, we need to flush now so that we can correctly detect
2596                // duplicates if there are multiple ValueSets in the same TX with the same URL
2597                // (which is an error, but we need to catch it). It'd be better to catch this by
2598                // inspecting the URLs in the bundle or something, since flushing hurts performance
2599                // but it's not expected that loading valuesets is going to be a huge high frequency
2600                // thing so it probably doesn't matter
2601                myEntityManager.flush();
2602
2603                ValidateUtil.isTrueOrThrowInvalidRequest(theResourceTable != null, "No resource supplied");
2604                if (isPlaceholder(theValueSet)) {
2605                        ourLog.info(
2606                                        "Not storing TermValueSet for placeholder {}",
2607                                        theValueSet.getIdElement().toVersionless().getValueAsString());
2608                        return;
2609                }
2610
2611                ValidateUtil.isNotBlankOrThrowUnprocessableEntity(
2612                                theValueSet.getUrl(), "ValueSet has no value for ValueSet.url");
2613                ourLog.info(
2614                                "Storing TermValueSet for {}",
2615                                theValueSet.getIdElement().toVersionless().getValueAsString());
2616
2617                /*
2618                 * Get CodeSystem and validate CodeSystemVersion
2619                 */
2620                TermValueSet termValueSet = new TermValueSet();
2621                termValueSet.setResource(theResourceTable);
2622                termValueSet.setUrl(theValueSet.getUrl());
2623                termValueSet.setVersion(theValueSet.getVersion());
2624                termValueSet.setName(theValueSet.hasName() ? theValueSet.getName() : null);
2625
2626                // Delete version being replaced
2627                Optional<TermValueSet> deletedTrmValueSet = deleteValueSetForResource(theResourceTable);
2628
2629                /*
2630                 * Do the upload.
2631                 */
2632                String url = termValueSet.getUrl();
2633                String version = termValueSet.getVersion();
2634                Optional<TermValueSet> optionalExistingTermValueSetByUrl;
2635
2636                if (deletedTrmValueSet.isPresent()
2637                                && Objects.equals(deletedTrmValueSet.get().getUrl(), url)
2638                                && Objects.equals(deletedTrmValueSet.get().getVersion(), version)) {
2639                        // If we just deleted the valueset marker, we don't need to check if it exists
2640                        // in the database
2641                        optionalExistingTermValueSetByUrl = Optional.empty();
2642                } else {
2643                        optionalExistingTermValueSetByUrl = getTermValueSet(version, url);
2644                }
2645
2646                if (optionalExistingTermValueSetByUrl.isEmpty()) {
2647
2648                        myEntityManager.persist(termValueSet);
2649
2650                } else {
2651                        TermValueSet existingTermValueSet = optionalExistingTermValueSetByUrl.get();
2652                        String msg;
2653                        if (version != null) {
2654                                msg = myContext
2655                                                .getLocalizer()
2656                                                .getMessage(
2657                                                                TermReadSvcImpl.class,
2658                                                                "cannotCreateDuplicateValueSetUrlAndVersion",
2659                                                                url,
2660                                                                version,
2661                                                                existingTermValueSet
2662                                                                                .getResource()
2663                                                                                .getIdDt()
2664                                                                                .toUnqualifiedVersionless()
2665                                                                                .getValue());
2666                        } else {
2667                                msg = myContext
2668                                                .getLocalizer()
2669                                                .getMessage(
2670                                                                TermReadSvcImpl.class,
2671                                                                "cannotCreateDuplicateValueSetUrl",
2672                                                                url,
2673                                                                existingTermValueSet
2674                                                                                .getResource()
2675                                                                                .getIdDt()
2676                                                                                .toUnqualifiedVersionless()
2677                                                                                .getValue());
2678                        }
2679                        throw new UnprocessableEntityException(Msg.code(902) + msg);
2680                }
2681        }
2682
2683        private Optional<TermValueSet> getTermValueSet(String version, String url) {
2684                Optional<TermValueSet> optionalExistingTermValueSetByUrl;
2685                if (version != null) {
2686                        optionalExistingTermValueSetByUrl = myTermValueSetDao.findTermValueSetByUrlAndVersion(url, version);
2687                } else {
2688                        optionalExistingTermValueSetByUrl = myTermValueSetDao.findTermValueSetByUrlAndNullVersion(url);
2689                }
2690                return optionalExistingTermValueSetByUrl;
2691        }
2692
2693        @Override
2694        @Transactional
2695        public IFhirResourceDaoCodeSystem.SubsumesResult subsumes(
2696                        IPrimitiveType<String> theCodeA,
2697                        IPrimitiveType<String> theCodeB,
2698                        IPrimitiveType<String> theSystem,
2699                        IBaseCoding theCodingA,
2700                        IBaseCoding theCodingB) {
2701                FhirVersionIndependentConcept conceptA = toConcept(theCodeA, theSystem, theCodingA);
2702                FhirVersionIndependentConcept conceptB = toConcept(theCodeB, theSystem, theCodingB);
2703
2704                if (!StringUtils.equals(conceptA.getSystem(), conceptB.getSystem())) {
2705                        throw new InvalidRequestException(
2706                                        Msg.code(903) + "Unable to test subsumption across different code systems");
2707                }
2708
2709                if (!StringUtils.equals(conceptA.getSystemVersion(), conceptB.getSystemVersion())) {
2710                        throw new InvalidRequestException(
2711                                        Msg.code(904) + "Unable to test subsumption across different code system versions");
2712                }
2713
2714                String codeASystemIdentifier;
2715                if (StringUtils.isNotEmpty(conceptA.getSystemVersion())) {
2716                        codeASystemIdentifier = conceptA.getSystem() + OUR_PIPE_CHARACTER + conceptA.getSystemVersion();
2717                } else {
2718                        codeASystemIdentifier = conceptA.getSystem();
2719                }
2720                TermConcept codeA = findCode(codeASystemIdentifier, conceptA.getCode())
2721                                .orElseThrow(() -> new InvalidRequestException("Unknown code: " + conceptA));
2722
2723                String codeBSystemIdentifier;
2724                if (StringUtils.isNotEmpty(conceptB.getSystemVersion())) {
2725                        codeBSystemIdentifier = conceptB.getSystem() + OUR_PIPE_CHARACTER + conceptB.getSystemVersion();
2726                } else {
2727                        codeBSystemIdentifier = conceptB.getSystem();
2728                }
2729                TermConcept codeB = findCode(codeBSystemIdentifier, conceptB.getCode())
2730                                .orElseThrow(() -> new InvalidRequestException("Unknown code: " + conceptB));
2731
2732                SearchSession searchSession = Search.session(myEntityManager);
2733
2734                ConceptSubsumptionOutcome subsumes;
2735                subsumes = testForSubsumption(searchSession, codeA, codeB, ConceptSubsumptionOutcome.SUBSUMES);
2736                if (subsumes == null) {
2737                        subsumes = testForSubsumption(searchSession, codeB, codeA, ConceptSubsumptionOutcome.SUBSUMEDBY);
2738                }
2739                if (subsumes == null) {
2740                        subsumes = ConceptSubsumptionOutcome.NOTSUBSUMED;
2741                }
2742
2743                return new IFhirResourceDaoCodeSystem.SubsumesResult(subsumes);
2744        }
2745
2746        @Override
2747        public IValidationSupport.LookupCodeResult lookupCode(
2748                        ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) {
2749                TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
2750                return txTemplate.execute(t -> {
2751                        final String theSystem = theLookupCodeRequest.getSystem();
2752                        final String theCode = theLookupCodeRequest.getCode();
2753                        Optional<TermConcept> codeOpt = findCode(theSystem, theCode);
2754                        if (codeOpt.isPresent()) {
2755                                TermConcept code = codeOpt.get();
2756
2757                                IValidationSupport.LookupCodeResult result = new IValidationSupport.LookupCodeResult();
2758                                result.setCodeSystemDisplayName(code.getCodeSystemVersion().getCodeSystemDisplayName());
2759                                result.setCodeSystemVersion(code.getCodeSystemVersion().getCodeSystemVersionId());
2760                                result.setSearchedForSystem(theSystem);
2761                                result.setSearchedForCode(theCode);
2762                                result.setFound(true);
2763                                result.setCodeDisplay(code.getDisplay());
2764
2765                                for (TermConceptDesignation next : code.getDesignations()) {
2766                                        // filter out the designation based on displayLanguage if any
2767                                        if (isDisplayLanguageMatch(theLookupCodeRequest.getDisplayLanguage(), next.getLanguage())) {
2768                                                IValidationSupport.ConceptDesignation designation = new IValidationSupport.ConceptDesignation();
2769                                                designation.setLanguage(next.getLanguage());
2770                                                designation.setUseSystem(next.getUseSystem());
2771                                                designation.setUseCode(next.getUseCode());
2772                                                designation.setUseDisplay(next.getUseDisplay());
2773                                                designation.setValue(next.getValue());
2774                                                result.getDesignations().add(designation);
2775                                        }
2776                                }
2777
2778                                final Collection<String> propertyNames = theLookupCodeRequest.getPropertyNames();
2779                                for (TermConceptProperty next : code.getProperties()) {
2780                                        if (ObjectUtils.isNotEmpty(propertyNames) && !propertyNames.contains(next.getKey())) {
2781                                                continue;
2782                                        }
2783                                        if (next.getType() == TermConceptPropertyTypeEnum.CODING) {
2784                                                IValidationSupport.CodingConceptProperty property =
2785                                                                new IValidationSupport.CodingConceptProperty(
2786                                                                                next.getKey(), next.getCodeSystem(), next.getValue(), next.getDisplay());
2787                                                result.getProperties().add(property);
2788                                        } else if (next.getType() == TermConceptPropertyTypeEnum.STRING) {
2789                                                IValidationSupport.StringConceptProperty property =
2790                                                                new IValidationSupport.StringConceptProperty(next.getKey(), next.getValue());
2791                                                result.getProperties().add(property);
2792                                        } else {
2793                                                throw new InternalErrorException(Msg.code(905) + "Unknown type: " + next.getType());
2794                                        }
2795                                }
2796
2797                                return result;
2798
2799                        } else {
2800                                return new LookupCodeResult().setFound(false);
2801                        }
2802                });
2803        }
2804
2805        @Nullable
2806        private ConceptSubsumptionOutcome testForSubsumption(
2807                        SearchSession theSearchSession,
2808                        TermConcept theLeft,
2809                        TermConcept theRight,
2810                        ConceptSubsumptionOutcome theOutput) {
2811                List<TermConcept> fetch = theSearchSession
2812                                .search(TermConcept.class)
2813                                .where(f -> f.bool()
2814                                                .must(f.match().field("myId").matching(theRight.getPid()))
2815                                                .must(f.match().field("myParentPids").matching(Long.toString(theLeft.getId()))))
2816                                .fetchHits(1);
2817
2818                if (fetch.size() > 0) {
2819                        return theOutput;
2820                } else {
2821                        return null;
2822                }
2823        }
2824
2825        private ArrayList<FhirVersionIndependentConcept> toVersionIndependentConcepts(
2826                        String theSystem, Set<TermConcept> codes) {
2827                ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>(codes.size());
2828                for (TermConcept next : codes) {
2829                        retVal.add(new FhirVersionIndependentConcept(theSystem, next.getCode()));
2830                }
2831                return retVal;
2832        }
2833
2834        @Override
2835        @Transactional
2836        public CodeValidationResult validateCodeInValueSet(
2837                        ValidationSupportContext theValidationSupportContext,
2838                        ConceptValidationOptions theOptions,
2839                        String theCodeSystem,
2840                        String theCode,
2841                        String theDisplay,
2842                        @Nonnull IBaseResource theValueSet) {
2843                invokeRunnableForUnitTest();
2844
2845                IPrimitiveType<?> urlPrimitive;
2846                if (theValueSet instanceof org.hl7.fhir.dstu2.model.ValueSet) {
2847                        urlPrimitive = FhirContext.forDstu2Hl7OrgCached()
2848                                        .newTerser()
2849                                        .getSingleValueOrNull(theValueSet, "url", IPrimitiveType.class);
2850                } else {
2851                        urlPrimitive = myContext.newTerser().getSingleValueOrNull(theValueSet, "url", IPrimitiveType.class);
2852                }
2853                String url = urlPrimitive.getValueAsString();
2854                if (isNotBlank(url)) {
2855                        return validateCode(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, url);
2856                }
2857                return null;
2858        }
2859
2860        @CoverageIgnore
2861        @Override
2862        public IValidationSupport.CodeValidationResult validateCode(
2863                        @Nonnull ValidationSupportContext theValidationSupportContext,
2864                        @Nonnull ConceptValidationOptions theOptions,
2865                        String theCodeSystemUrl,
2866                        String theCode,
2867                        String theDisplay,
2868                        String theValueSetUrl) {
2869                // TODO GGG TRY TO JUST AUTO_PASS HERE AND SEE WHAT HAPPENS.
2870                invokeRunnableForUnitTest();
2871                theOptions.setValidateDisplay(isNotBlank(theDisplay));
2872
2873                if (isNotBlank(theValueSetUrl)) {
2874                        return validateCodeInValueSet(
2875                                        theValidationSupportContext, theOptions, theValueSetUrl, theCodeSystemUrl, theCode, theDisplay);
2876                }
2877
2878                TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
2879                txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
2880                txTemplate.setReadOnly(true);
2881                Optional<FhirVersionIndependentConcept> codeOpt =
2882                                txTemplate.execute(tx -> findCode(theCodeSystemUrl, theCode).map(c -> {
2883                                        String codeSystemVersionId = getCurrentCodeSystemVersion(
2884                                                                        theValidationSupportContext, theCodeSystemUrl)
2885                                                        .myCodeSystemVersionId;
2886                                        return new FhirVersionIndependentConcept(
2887                                                        theCodeSystemUrl, c.getCode(), c.getDisplay(), codeSystemVersionId);
2888                                }));
2889
2890                if (codeOpt != null && codeOpt.isPresent()) {
2891                        FhirVersionIndependentConcept code = codeOpt.get();
2892                        if (!theOptions.isValidateDisplay()
2893                                        || isBlank(code.getDisplay())
2894                                        || isBlank(theDisplay)
2895                                        || code.getDisplay().equals(theDisplay)) {
2896                                return new CodeValidationResult().setCode(code.getCode()).setDisplay(code.getDisplay());
2897                        } else {
2898                                return InMemoryTerminologyServerValidationSupport.createResultForDisplayMismatch(
2899                                                myContext,
2900                                                theCode,
2901                                                theDisplay,
2902                                                code.getDisplay(),
2903                                                code.getSystem(),
2904                                                code.getSystemVersion(),
2905                                                myStorageSettings.getIssueSeverityForCodeDisplayMismatch());
2906                        }
2907                }
2908
2909                return createCodeNotFoundErrorForValidationResult(
2910                                theCodeSystemUrl, theCode, null, createMessageAppendForCodeNotFoundInCodeSystem(theCodeSystemUrl));
2911        }
2912
2913        IValidationSupport.CodeValidationResult validateCodeInValueSet(
2914                        ValidationSupportContext theValidationSupportContext,
2915                        ConceptValidationOptions theValidationOptions,
2916                        String theValueSetUrl,
2917                        String theCodeSystem,
2918                        String theCode,
2919                        String theDisplay) {
2920                IBaseResource valueSet =
2921                                theValidationSupportContext.getRootValidationSupport().fetchValueSet(theValueSetUrl);
2922                CodeValidationResult retVal = null;
2923
2924                // If we don't have a PID, this came from some source other than the JPA
2925                // database, so we don't need to check if it's pre-expanded or not
2926                if (valueSet instanceof IAnyResource) {
2927                        JpaPid pid = IDao.RESOURCE_PID.get(valueSet);
2928                        if (pid != null) {
2929                                TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
2930                                retVal = txTemplate.execute(tx -> {
2931                                        if (isValueSetPreExpandedForCodeValidation(valueSet)) {
2932                                                return validateCodeIsInPreExpandedValueSet(
2933                                                                theValidationSupportContext,
2934                                                                theValidationOptions,
2935                                                                valueSet,
2936                                                                theCodeSystem,
2937                                                                theCode,
2938                                                                theDisplay,
2939                                                                null,
2940                                                                null);
2941                                        } else {
2942                                                return null;
2943                                        }
2944                                });
2945                        }
2946                }
2947
2948                if (retVal == null) {
2949                        if (valueSet != null) {
2950                                retVal = myInMemoryTerminologyServerValidationSupport.validateCodeInValueSet(
2951                                                theValidationSupportContext,
2952                                                theValidationOptions,
2953                                                theCodeSystem,
2954                                                theCode,
2955                                                theDisplay,
2956                                                valueSet);
2957                        } else {
2958                                String append = " - Unable to locate ValueSet[" + theValueSetUrl + "]";
2959                                retVal = createCodeNotFoundErrorForValidationResult(theCodeSystem, theCode, null, append);
2960                        }
2961                }
2962
2963                // Check if someone is accidentally using a VS url where it should be a CS URL
2964                if (retVal != null
2965                                && retVal.getCode() == null
2966                                && theCodeSystem != null
2967                                && myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) {
2968                        if (isValueSetSupported(theValidationSupportContext, theCodeSystem)) {
2969                                if (!isCodeSystemSupported(theValidationSupportContext, theCodeSystem)) {
2970                                        String newMessage = "Unable to validate code " + theCodeSystem + "#" + theCode
2971                                                        + " - Supplied system URL is a ValueSet URL and not a CodeSystem URL, check if it is correct: "
2972                                                        + theCodeSystem;
2973                                        retVal.setMessage(newMessage);
2974                                }
2975                        }
2976                }
2977
2978                return retVal;
2979        }
2980
2981        @Override
2982        public CodeSystem fetchCanonicalCodeSystemFromCompleteContext(String theSystem) {
2983                IValidationSupport validationSupport = provideValidationSupport();
2984                IBaseResource codeSystem = validationSupport.fetchCodeSystem(theSystem);
2985                if (codeSystem != null) {
2986                        codeSystem = myVersionCanonicalizer.codeSystemToCanonical(codeSystem);
2987                }
2988                return (CodeSystem) codeSystem;
2989        }
2990
2991        @Nonnull
2992        private IValidationSupport provideJpaValidationSupport() {
2993                IValidationSupport jpaValidationSupport = myJpaValidationSupport;
2994                if (jpaValidationSupport == null) {
2995                        jpaValidationSupport = myApplicationContext.getBean("myJpaValidationSupport", IValidationSupport.class);
2996                        myJpaValidationSupport = jpaValidationSupport;
2997                }
2998                return jpaValidationSupport;
2999        }
3000
3001        @Nonnull
3002        protected IValidationSupport provideValidationSupport() {
3003                IValidationSupport validationSupport = myValidationSupport;
3004                if (validationSupport == null) {
3005                        validationSupport = myApplicationContext.getBean(IValidationSupport.class);
3006                        myValidationSupport = validationSupport;
3007                }
3008                return validationSupport;
3009        }
3010
3011        public ValueSet fetchCanonicalValueSetFromCompleteContext(String theSystem) {
3012                IValidationSupport validationSupport = provideValidationSupport();
3013                IBaseResource valueSet = validationSupport.fetchValueSet(theSystem);
3014                if (valueSet != null) {
3015                        valueSet = myVersionCanonicalizer.valueSetToCanonical(valueSet);
3016                }
3017                return (ValueSet) valueSet;
3018        }
3019
3020        @Override
3021        public IBaseResource fetchValueSet(String theValueSetUrl) {
3022                return provideJpaValidationSupport().fetchValueSet(theValueSetUrl);
3023        }
3024
3025        @Override
3026        public FhirContext getFhirContext() {
3027                return myContext;
3028        }
3029
3030        private void findCodesAbove(
3031                        CodeSystem theSystem,
3032                        String theSystemString,
3033                        String theCode,
3034                        List<FhirVersionIndependentConcept> theListToPopulate) {
3035                List<CodeSystem.ConceptDefinitionComponent> conceptList = theSystem.getConcept();
3036                for (CodeSystem.ConceptDefinitionComponent next : conceptList) {
3037                        addTreeIfItContainsCode(theSystemString, next, theCode, theListToPopulate);
3038                }
3039        }
3040
3041        @Override
3042        public List<FhirVersionIndependentConcept> findCodesAboveUsingBuiltInSystems(String theSystem, String theCode) {
3043                ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>();
3044                CodeSystem system = fetchCanonicalCodeSystemFromCompleteContext(theSystem);
3045                if (system != null) {
3046                        findCodesAbove(system, theSystem, theCode, retVal);
3047                }
3048                return retVal;
3049        }
3050
3051        private void findCodesBelow(
3052                        CodeSystem theSystem,
3053                        String theSystemString,
3054                        String theCode,
3055                        List<FhirVersionIndependentConcept> theListToPopulate) {
3056                List<CodeSystem.ConceptDefinitionComponent> conceptList = theSystem.getConcept();
3057                findCodesBelow(theSystemString, theCode, theListToPopulate, conceptList);
3058        }
3059
3060        private void findCodesBelow(
3061                        String theSystemString,
3062                        String theCode,
3063                        List<FhirVersionIndependentConcept> theListToPopulate,
3064                        List<CodeSystem.ConceptDefinitionComponent> conceptList) {
3065                for (CodeSystem.ConceptDefinitionComponent next : conceptList) {
3066                        if (theCode.equals(next.getCode())) {
3067                                addAllChildren(theSystemString, next, theListToPopulate);
3068                        } else {
3069                                findCodesBelow(theSystemString, theCode, theListToPopulate, next.getConcept());
3070                        }
3071                }
3072        }
3073
3074        @Override
3075        public List<FhirVersionIndependentConcept> findCodesBelowUsingBuiltInSystems(String theSystem, String theCode) {
3076                ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>();
3077                CodeSystem system = fetchCanonicalCodeSystemFromCompleteContext(theSystem);
3078                if (system != null) {
3079                        findCodesBelow(system, theSystem, theCode, retVal);
3080                }
3081                return retVal;
3082        }
3083
3084        private void addAllChildren(
3085                        String theSystemString,
3086                        CodeSystem.ConceptDefinitionComponent theCode,
3087                        List<FhirVersionIndependentConcept> theListToPopulate) {
3088                if (isNotBlank(theCode.getCode())) {
3089                        theListToPopulate.add(new FhirVersionIndependentConcept(theSystemString, theCode.getCode()));
3090                }
3091                for (CodeSystem.ConceptDefinitionComponent nextChild : theCode.getConcept()) {
3092                        addAllChildren(theSystemString, nextChild, theListToPopulate);
3093                }
3094        }
3095
3096        private boolean addTreeIfItContainsCode(
3097                        String theSystemString,
3098                        CodeSystem.ConceptDefinitionComponent theNext,
3099                        String theCode,
3100                        List<FhirVersionIndependentConcept> theListToPopulate) {
3101                boolean foundCodeInChild = false;
3102                for (CodeSystem.ConceptDefinitionComponent nextChild : theNext.getConcept()) {
3103                        foundCodeInChild |= addTreeIfItContainsCode(theSystemString, nextChild, theCode, theListToPopulate);
3104                }
3105
3106                if (theCode.equals(theNext.getCode()) || foundCodeInChild) {
3107                        theListToPopulate.add(new FhirVersionIndependentConcept(theSystemString, theNext.getCode()));
3108                        return true;
3109                }
3110
3111                return false;
3112        }
3113
3114        @Nonnull
3115        private FhirVersionIndependentConcept toConcept(
3116                        IPrimitiveType<String> theCodeType,
3117                        IPrimitiveType<String> theCodeSystemIdentifierType,
3118                        IBaseCoding theCodingType) {
3119                String code = theCodeType != null ? theCodeType.getValueAsString() : null;
3120                String system = theCodeSystemIdentifierType != null
3121                                ? getUrlFromIdentifier(theCodeSystemIdentifierType.getValueAsString())
3122                                : null;
3123                String systemVersion = theCodeSystemIdentifierType != null
3124                                ? getVersionFromIdentifier(theCodeSystemIdentifierType.getValueAsString())
3125                                : null;
3126                if (theCodingType != null) {
3127                        Coding canonicalizedCoding = myVersionCanonicalizer.codingToCanonical(theCodingType);
3128                        assert canonicalizedCoding != null; // Shouldn't be null, since theCodingType isn't
3129                        code = canonicalizedCoding.getCode();
3130                        system = canonicalizedCoding.getSystem();
3131                        systemVersion = canonicalizedCoding.getVersion();
3132                }
3133                return new FhirVersionIndependentConcept(system, code, null, systemVersion);
3134        }
3135
3136        /**
3137         * When the search is for unversioned loinc system it uses the forcedId to obtain the current
3138         * version, as it is not necessarily the last  one anymore.
3139         * For other cases it keeps on considering the last uploaded as the current
3140         */
3141        @Override
3142        public Optional<TermValueSet> findCurrentTermValueSet(String theUrl) {
3143                if (TermReadSvcUtil.isLoincUnversionedValueSet(theUrl)) {
3144                        Optional<String> vsIdOpt = TermReadSvcUtil.getValueSetId(theUrl);
3145                        if (vsIdOpt.isEmpty()) {
3146                                return Optional.empty();
3147                        }
3148
3149                        return myTermValueSetDao.findTermValueSetByForcedId(vsIdOpt.get());
3150                }
3151
3152                List<TermValueSet> termValueSetList = myTermValueSetDao.findTermValueSetByUrl(Pageable.ofSize(1), theUrl);
3153                if (termValueSetList.isEmpty()) {
3154                        return Optional.empty();
3155                }
3156
3157                return Optional.of(termValueSetList.get(0));
3158        }
3159
3160        @Override
3161        public Optional<IBaseResource> readCodeSystemByForcedId(String theForcedId) {
3162                @SuppressWarnings("unchecked")
3163                List<ResourceTable> resultList = (List<ResourceTable>) myEntityManager
3164                                .createQuery("select r from ResourceTable r "
3165                                                + "where r.myResourceType = 'CodeSystem' and r.myFhirId = :fhirId")
3166                                .setParameter("fhirId", theForcedId)
3167                                .getResultList();
3168                if (resultList.isEmpty()) return Optional.empty();
3169
3170                if (resultList.size() > 1)
3171                        throw new NonUniqueResultException(Msg.code(911) + "More than one CodeSystem is pointed by forcedId: "
3172                                        + theForcedId + ". Was constraint " + ResourceTable.IDX_RES_TYPE_FHIR_ID + " removed?");
3173
3174                IFhirResourceDao<CodeSystem> csDao = myDaoRegistry.getResourceDao("CodeSystem");
3175                IBaseResource cs = myJpaStorageResourceParser.toResource(resultList.get(0), false);
3176                return Optional.of(cs);
3177        }
3178
3179        @Transactional
3180        @Override
3181        public ReindexTerminologyResult reindexTerminology() throws InterruptedException {
3182                if (myFulltextSearchSvc == null) {
3183                        return ReindexTerminologyResult.SEARCH_SVC_DISABLED;
3184                }
3185
3186                if (isBatchTerminologyTasksRunning()) {
3187                        return ReindexTerminologyResult.OTHER_BATCH_TERMINOLOGY_TASKS_RUNNING;
3188                }
3189
3190                // disallow pre-expanding ValueSets while reindexing
3191                myDeferredStorageSvc.setProcessDeferred(false);
3192
3193                int objectLoadingThreadNumber = calculateObjectLoadingThreadNumber();
3194                ourLog.info("Using {} threads to load objects", objectLoadingThreadNumber);
3195
3196                try {
3197                        SearchSession searchSession = getSearchSession();
3198                        searchSession
3199                                        .massIndexer(TermConcept.class)
3200                                        .dropAndCreateSchemaOnStart(true)
3201                                        .purgeAllOnStart(false)
3202                                        .batchSizeToLoadObjects(100)
3203                                        .cacheMode(CacheMode.IGNORE)
3204                                        .threadsToLoadObjects(6)
3205                                        .transactionTimeout(60 * SECONDS_IN_MINUTE)
3206                                        .monitor(new PojoMassIndexingLoggingMonitor(INDEXED_ROOTS_LOGGING_COUNT))
3207                                        .startAndWait();
3208                } finally {
3209                        myDeferredStorageSvc.setProcessDeferred(true);
3210                }
3211
3212                return ReindexTerminologyResult.SUCCESS;
3213        }
3214
3215        @VisibleForTesting
3216        boolean isBatchTerminologyTasksRunning() {
3217                return isNotSafeToPreExpandValueSets() || isPreExpandingValueSets();
3218        }
3219
3220        @VisibleForTesting
3221        int calculateObjectLoadingThreadNumber() {
3222                IConnectionPoolInfoProvider connectionPoolInfoProvider =
3223                                new ConnectionPoolInfoProvider(myHibernatePropertiesProvider.getDataSource());
3224                Optional<Integer> maxConnectionsOpt = connectionPoolInfoProvider.getTotalConnectionSize();
3225                if (maxConnectionsOpt.isEmpty()) {
3226                        return DEFAULT_MASS_INDEXER_OBJECT_LOADING_THREADS;
3227                }
3228
3229                int maxConnections = maxConnectionsOpt.get();
3230                int usableThreads = maxConnections < 6 ? 1 : maxConnections - 5;
3231                int objectThreads = Math.min(usableThreads, MAX_MASS_INDEXER_OBJECT_LOADING_THREADS);
3232                ourLog.debug(
3233                                "Data source connection pool has {} connections allocated, so reindexing will use {} object "
3234                                                + "loading threads (each using a connection)",
3235                                maxConnections,
3236                                objectThreads);
3237                return objectThreads;
3238        }
3239
3240        @VisibleForTesting
3241        SearchSession getSearchSession() {
3242                return Search.session(myEntityManager);
3243        }
3244
3245        @Override
3246        public ValueSetExpansionOutcome expandValueSet(
3247                        ValidationSupportContext theValidationSupportContext,
3248                        ValueSetExpansionOptions theExpansionOptions,
3249                        @Nonnull IBaseResource theValueSetToExpand) {
3250                ValueSet canonicalInput = myVersionCanonicalizer.valueSetToCanonical(theValueSetToExpand);
3251                org.hl7.fhir.r4.model.ValueSet expandedR4 = expandValueSet(theExpansionOptions, canonicalInput);
3252                return new ValueSetExpansionOutcome(myVersionCanonicalizer.valueSetFromCanonical(expandedR4));
3253        }
3254
3255        @Override
3256        public IBaseResource expandValueSet(ValueSetExpansionOptions theExpansionOptions, IBaseResource theInput) {
3257                org.hl7.fhir.r4.model.ValueSet valueSetToExpand = myVersionCanonicalizer.valueSetToCanonical(theInput);
3258                org.hl7.fhir.r4.model.ValueSet valueSetR4 = expandValueSet(theExpansionOptions, valueSetToExpand);
3259                return myVersionCanonicalizer.valueSetFromCanonical(valueSetR4);
3260        }
3261
3262        @Override
3263        public void expandValueSet(
3264                        ValueSetExpansionOptions theExpansionOptions,
3265                        IBaseResource theValueSetToExpand,
3266                        IValueSetConceptAccumulator theValueSetCodeAccumulator) {
3267                org.hl7.fhir.r4.model.ValueSet valueSetToExpand =
3268                                myVersionCanonicalizer.valueSetToCanonical(theValueSetToExpand);
3269                expandValueSet(theExpansionOptions, valueSetToExpand, theValueSetCodeAccumulator);
3270        }
3271
3272        private org.hl7.fhir.r4.model.ValueSet getValueSetFromResourceTable(ResourceTable theResourceTable) {
3273                Class<? extends IBaseResource> type =
3274                                getFhirContext().getResourceDefinition("ValueSet").getImplementingClass();
3275                IBaseResource valueSet = myJpaStorageResourceParser.toResource(type, theResourceTable, null, false);
3276                return myVersionCanonicalizer.valueSetToCanonical(valueSet);
3277        }
3278
3279        @Override
3280        public CodeValidationResult validateCodeIsInPreExpandedValueSet(
3281                        ValidationSupportContext theValidationSupportContext,
3282                        ConceptValidationOptions theOptions,
3283                        IBaseResource theValueSet,
3284                        String theSystem,
3285                        String theCode,
3286                        String theDisplay,
3287                        IBaseDatatype theCoding,
3288                        IBaseDatatype theCodeableConcept) {
3289                ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSet, "ValueSet must not be null");
3290                org.hl7.fhir.r4.model.ValueSet valueSetR4 = myVersionCanonicalizer.valueSetToCanonical(theValueSet);
3291                org.hl7.fhir.r4.model.Coding codingR4 = myVersionCanonicalizer.codingToCanonical((IBaseCoding) theCoding);
3292                org.hl7.fhir.r4.model.CodeableConcept codeableConcept =
3293                                myVersionCanonicalizer.codeableConceptToCanonical(theCodeableConcept);
3294
3295                return validateCodeIsInPreExpandedValueSet(
3296                                theValidationSupportContext,
3297                                theOptions,
3298                                valueSetR4,
3299                                theSystem,
3300                                theCode,
3301                                theDisplay,
3302                                codingR4,
3303                                codeableConcept);
3304        }
3305
3306        @Override
3307        public boolean isValueSetPreExpandedForCodeValidation(IBaseResource theValueSet) {
3308                ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSet, "ValueSet must not be null");
3309                org.hl7.fhir.r4.model.ValueSet valueSetR4 = myVersionCanonicalizer.valueSetToCanonical(theValueSet);
3310                return isValueSetPreExpandedForCodeValidation(valueSetR4);
3311        }
3312
3313        private static class TermCodeSystemVersionDetails {
3314
3315                private final long myPid;
3316                private final String myCodeSystemVersionId;
3317
3318                public TermCodeSystemVersionDetails(long thePid, String theCodeSystemVersionId) {
3319                        myPid = thePid;
3320                        myCodeSystemVersionId = theCodeSystemVersionId;
3321                }
3322        }
3323
3324        public static class Job implements HapiJob {
3325                @Autowired
3326                private ITermReadSvc myTerminologySvc;
3327
3328                @Override
3329                public void execute(JobExecutionContext theContext) {
3330                        myTerminologySvc.preExpandDeferredValueSetsToTerminologyTables();
3331                }
3332        }
3333
3334        /**
3335         * Properties returned from method buildSearchScroll
3336         */
3337        private static final class SearchProperties {
3338                private final List<Supplier<SearchScroll<EntityReference>>> mySearchScroll = new ArrayList<>();
3339                private List<String> myIncludeOrExcludeCodes;
3340
3341                public List<Supplier<SearchScroll<EntityReference>>> getSearchScroll() {
3342                        return mySearchScroll;
3343                }
3344
3345                public void addSearchScroll(Supplier<SearchScroll<EntityReference>> theSearchScrollSupplier) {
3346                        mySearchScroll.add(theSearchScrollSupplier);
3347                }
3348
3349                public List<String> getIncludeOrExcludeCodes() {
3350                        return myIncludeOrExcludeCodes;
3351                }
3352
3353                public void setIncludeOrExcludeCodes(List<String> theIncludeOrExcludeCodes) {
3354                        myIncludeOrExcludeCodes = theIncludeOrExcludeCodes;
3355                }
3356
3357                public boolean hasIncludeOrExcludeCodes() {
3358                        return !myIncludeOrExcludeCodes.isEmpty();
3359                }
3360        }
3361
3362        static boolean isValueSetDisplayLanguageMatch(ValueSetExpansionOptions theExpansionOptions, String theStoredLang) {
3363                if (theExpansionOptions == null) {
3364                        return true;
3365                }
3366
3367                if (theExpansionOptions.getTheDisplayLanguage() == null || theStoredLang == null) {
3368                        return true;
3369                }
3370
3371                return theExpansionOptions.getTheDisplayLanguage().equalsIgnoreCase(theStoredLang);
3372        }
3373
3374        @Nonnull
3375        private static String createMessageAppendForCodeNotFoundInCodeSystem(String theCodeSystemUrl) {
3376                return " - Code is not found in CodeSystem: " + theCodeSystemUrl;
3377        }
3378
3379        @VisibleForTesting
3380        public static void setForceDisableHibernateSearchForUnitTest(boolean theForceDisableHibernateSearchForUnitTest) {
3381                ourForceDisableHibernateSearchForUnitTest = theForceDisableHibernateSearchForUnitTest;
3382        }
3383
3384        static boolean isPlaceholder(DomainResource theResource) {
3385                boolean retVal = false;
3386                Extension extension = theResource.getExtensionByUrl(HapiExtensions.EXT_RESOURCE_PLACEHOLDER);
3387                if (extension != null && extension.hasValue() && extension.getValue() instanceof BooleanType) {
3388                        retVal = ((BooleanType) extension.getValue()).booleanValue();
3389                }
3390                return retVal;
3391        }
3392
3393        /**
3394         * This is only used for unit tests to test failure conditions
3395         */
3396        static void invokeRunnableForUnitTest() {
3397                if (myInvokeOnNextCallForUnitTest != null) {
3398                        Runnable invokeOnNextCallForUnitTest = myInvokeOnNextCallForUnitTest;
3399                        myInvokeOnNextCallForUnitTest = null;
3400                        invokeOnNextCallForUnitTest.run();
3401                }
3402        }
3403
3404        @VisibleForTesting
3405        public static void setInvokeOnNextCallForUnitTest(Runnable theInvokeOnNextCallForUnitTest) {
3406                myInvokeOnNextCallForUnitTest = theInvokeOnNextCallForUnitTest;
3407        }
3408
3409        static List<TermConcept> toPersistedConcepts(
3410                        List<CodeSystem.ConceptDefinitionComponent> theConcept, TermCodeSystemVersion theCodeSystemVersion) {
3411                ArrayList<TermConcept> retVal = new ArrayList<>();
3412
3413                for (CodeSystem.ConceptDefinitionComponent next : theConcept) {
3414                        if (isNotBlank(next.getCode())) {
3415                                TermConcept termConcept = toTermConcept(next, theCodeSystemVersion);
3416                                retVal.add(termConcept);
3417                        }
3418                }
3419
3420                return retVal;
3421        }
3422
3423        @Nonnull
3424        static TermConcept toTermConcept(
3425                        CodeSystem.ConceptDefinitionComponent theConceptDefinition, TermCodeSystemVersion theCodeSystemVersion) {
3426                TermConcept termConcept = new TermConcept();
3427                termConcept.setCode(theConceptDefinition.getCode());
3428                termConcept.setCodeSystemVersion(theCodeSystemVersion);
3429                termConcept.setDisplay(theConceptDefinition.getDisplay());
3430
3431                termConcept.addChildren(
3432                                toPersistedConcepts(theConceptDefinition.getConcept(), theCodeSystemVersion), RelationshipTypeEnum.ISA);
3433
3434                for (CodeSystem.ConceptDefinitionDesignationComponent designationComponent :
3435                                theConceptDefinition.getDesignation()) {
3436                        if (isNotBlank(designationComponent.getValue())) {
3437                                TermConceptDesignation designation = termConcept.addDesignation();
3438                                designation.setLanguage(designationComponent.hasLanguage() ? designationComponent.getLanguage() : null);
3439                                if (designationComponent.hasUse()) {
3440                                        designation.setUseSystem(
3441                                                        designationComponent.getUse().hasSystem()
3442                                                                        ? designationComponent.getUse().getSystem()
3443                                                                        : null);
3444                                        designation.setUseCode(
3445                                                        designationComponent.getUse().hasCode()
3446                                                                        ? designationComponent.getUse().getCode()
3447                                                                        : null);
3448                                        designation.setUseDisplay(
3449                                                        designationComponent.getUse().hasDisplay()
3450                                                                        ? designationComponent.getUse().getDisplay()
3451                                                                        : null);
3452                                }
3453                                designation.setValue(designationComponent.getValue());
3454                        }
3455                }
3456
3457                for (CodeSystem.ConceptPropertyComponent next : theConceptDefinition.getProperty()) {
3458                        TermConceptProperty property = new TermConceptProperty();
3459
3460                        property.setKey(next.getCode());
3461                        property.setConcept(termConcept);
3462                        property.setCodeSystemVersion(theCodeSystemVersion);
3463
3464                        if (next.getValue() instanceof StringType) {
3465                                property.setType(TermConceptPropertyTypeEnum.STRING);
3466                                property.setValue(next.getValueStringType().getValue());
3467                        } else if (next.getValue() instanceof BooleanType) {
3468                                property.setType(TermConceptPropertyTypeEnum.BOOLEAN);
3469                                property.setValue(((BooleanType) next.getValue()).getValueAsString());
3470                        } else if (next.getValue() instanceof IntegerType) {
3471                                property.setType(TermConceptPropertyTypeEnum.INTEGER);
3472                                property.setValue(((IntegerType) next.getValue()).getValueAsString());
3473                        } else if (next.getValue() instanceof DecimalType) {
3474                                property.setType(TermConceptPropertyTypeEnum.DECIMAL);
3475                                property.setValue(((DecimalType) next.getValue()).getValueAsString());
3476                        } else if (next.getValue() instanceof DateTimeType) {
3477                                // DateType is not supported because it's not
3478                                // supported in CodeSystem.setValue
3479                                property.setType(TermConceptPropertyTypeEnum.DATETIME);
3480                                property.setValue(((DateTimeType) next.getValue()).getValueAsString());
3481                        } else if (next.getValue() instanceof Coding) {
3482                                Coding nextCoding = next.getValueCoding();
3483                                property.setType(TermConceptPropertyTypeEnum.CODING);
3484                                property.setCodeSystem(nextCoding.getSystem());
3485                                property.setValue(nextCoding.getCode());
3486                                property.setDisplay(nextCoding.getDisplay());
3487                        } else if (next.getValue() != null) {
3488                                ourLog.warn("Don't know how to handle properties of type: "
3489                                                + next.getValue().getClass());
3490                                continue;
3491                        }
3492
3493                        termConcept.getProperties().add(property);
3494                }
3495                return termConcept;
3496        }
3497
3498        static boolean isDisplayLanguageMatch(String theReqLang, String theStoredLang) {
3499                // NOTE: return the designation when one of then is not specified.
3500                if (theReqLang == null || theStoredLang == null) return true;
3501
3502                return theReqLang.equalsIgnoreCase(theStoredLang);
3503        }
3504}