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 createFailureCodeValidationResult(theSystem, theCode, null, append);
2206        }
2207
2208        private CodeValidationResult createFailureCodeValidationResult(
2209                        String theSystem, String theCode, String theCodeSystemVersion, String theAppend) {
2210                String theMessage = "Unable to validate code " + theSystem + "#" + theCode + theAppend;
2211                return new CodeValidationResult()
2212                                .setSeverity(IssueSeverity.ERROR)
2213                                .setCodeSystemVersion(theCodeSystemVersion)
2214                                .setMessage(theMessage)
2215                                .addIssue(new CodeValidationIssue(
2216                                                theMessage,
2217                                                IssueSeverity.ERROR,
2218                                                CodeValidationIssueCode.CODE_INVALID,
2219                                                CodeValidationIssueCoding.INVALID_CODE));
2220        }
2221
2222        private List<TermValueSetConcept> findByValueSetResourcePidSystemAndCode(
2223                        JpaPid theResourcePid, String theSystem, String theCode) {
2224                assert TransactionSynchronizationManager.isSynchronizationActive();
2225
2226                List<TermValueSetConcept> retVal = new ArrayList<>();
2227                Optional<TermValueSetConcept> optionalTermValueSetConcept;
2228                int versionIndex = theSystem.indexOf(OUR_PIPE_CHARACTER);
2229                if (versionIndex >= 0) {
2230                        String systemUrl = theSystem.substring(0, versionIndex);
2231                        String systemVersion = theSystem.substring(versionIndex + 1);
2232                        optionalTermValueSetConcept = myValueSetConceptDao.findByValueSetResourcePidSystemAndCodeWithVersion(
2233                                        theResourcePid.getId(), systemUrl, systemVersion, theCode);
2234                } else {
2235                        optionalTermValueSetConcept = myValueSetConceptDao.findByValueSetResourcePidSystemAndCode(
2236                                        theResourcePid.getId(), theSystem, theCode);
2237                }
2238                optionalTermValueSetConcept.ifPresent(retVal::add);
2239                return retVal;
2240        }
2241
2242        private void fetchChildren(TermConcept theConcept, Set<TermConcept> theSetToPopulate) {
2243                for (TermConceptParentChildLink nextChildLink : theConcept.getChildren()) {
2244                        TermConcept nextChild = nextChildLink.getChild();
2245                        if (addToSet(theSetToPopulate, nextChild)) {
2246                                fetchChildren(nextChild, theSetToPopulate);
2247                        }
2248                }
2249        }
2250
2251        private Optional<TermConcept> fetchLoadedCode(Long theCodeSystemResourcePid, String theCode) {
2252                TermCodeSystemVersion codeSystem =
2253                                myCodeSystemVersionDao.findCurrentVersionForCodeSystemResourcePid(theCodeSystemResourcePid);
2254                return myConceptDao.findByCodeSystemAndCode(codeSystem.getPid(), theCode);
2255        }
2256
2257        private void fetchParents(TermConcept theConcept, Set<TermConcept> theSetToPopulate) {
2258                for (TermConceptParentChildLink nextChildLink : theConcept.getParents()) {
2259                        TermConcept nextChild = nextChildLink.getParent();
2260                        if (addToSet(theSetToPopulate, nextChild)) {
2261                                fetchParents(nextChild, theSetToPopulate);
2262                        }
2263                }
2264        }
2265
2266        @Override
2267        public Optional<TermConcept> findCode(String theCodeSystem, String theCode) {
2268                /*
2269                 * Loading concepts without a transaction causes issues later on some
2270                 * platforms (e.g. PSQL) so make sure that we always call this with an open transaction
2271                 */
2272                HapiTransactionService.requireTransaction();
2273
2274                TermCodeSystemVersionDetails csv =
2275                                getCurrentCodeSystemVersion(new ValidationSupportContext(provideValidationSupport()), theCodeSystem);
2276                if (csv == null) {
2277                        return Optional.empty();
2278                }
2279                return myConceptDao.findByCodeSystemAndCode(csv.myPid, theCode);
2280        }
2281
2282        @Override
2283        public List<TermConcept> findCodes(String theCodeSystem, List<String> theCodeList) {
2284                HapiTransactionService.requireTransaction();
2285
2286                TermCodeSystemVersionDetails csv =
2287                                getCurrentCodeSystemVersion(new ValidationSupportContext(provideValidationSupport()), theCodeSystem);
2288                if (csv == null) {
2289                        return Collections.emptyList();
2290                }
2291
2292                return myConceptDao.findByCodeSystemAndCodeList(csv.myPid, theCodeList);
2293        }
2294
2295        @Nullable
2296        private TermCodeSystemVersionDetails getCurrentCodeSystemVersion(
2297                        ValidationSupportContext theValidationSupportContext, String theCodeSystemIdentifier) {
2298                String version = getVersionFromIdentifier(theCodeSystemIdentifier);
2299
2300                // Fetch the CodeSystem from ValidationSupport, which should return a cached copy. We
2301                // keep a copy of the current version entity in userData in that cached copy
2302                // to avoid repeated lookups
2303                TermCodeSystemVersionDetails retVal;
2304                IBaseResource codeSystem =
2305                                theValidationSupportContext.getRootValidationSupport().fetchCodeSystem(theCodeSystemIdentifier);
2306                if (codeSystem != null) {
2307
2308                        synchronized (codeSystem) {
2309                                retVal = (TermCodeSystemVersionDetails) codeSystem.getUserData(CS_USERDATA_CURRENT_VERSION);
2310                                if (retVal == null) {
2311                                        retVal = getCurrentCodeSystemVersion(theCodeSystemIdentifier, version);
2312                                        codeSystem.setUserData(CS_USERDATA_CURRENT_VERSION, retVal);
2313                                }
2314                        }
2315                } else {
2316                        retVal = getCurrentCodeSystemVersion(theCodeSystemIdentifier, version);
2317                }
2318
2319                return retVal;
2320        }
2321
2322        @Nullable
2323        private TermCodeSystemVersionDetails getCurrentCodeSystemVersion(String theCodeSystemIdentifier, String version) {
2324                TermCodeSystemVersionDetails retVal;
2325                retVal = myTxTemplate.execute(tx -> {
2326                        TermCodeSystemVersion csv = null;
2327                        TermCodeSystem cs = myCodeSystemDao.findByCodeSystemUri(getUrlFromIdentifier(theCodeSystemIdentifier));
2328                        if (cs != null) {
2329                                if (version != null) {
2330                                        csv = myCodeSystemVersionDao.findByCodeSystemPidAndVersion(cs.getPid(), version);
2331                                } else if (cs.getCurrentVersion() != null) {
2332                                        csv = cs.getCurrentVersion();
2333                                }
2334                        }
2335                        if (csv != null) {
2336                                return new TermCodeSystemVersionDetails(csv.getPid(), csv.getCodeSystemVersionId());
2337                        } else {
2338                                return null;
2339                        }
2340                });
2341                return retVal;
2342        }
2343
2344        private String getVersionFromIdentifier(String theUri) {
2345                String retVal = null;
2346                if (StringUtils.isNotEmpty((theUri))) {
2347                        int versionSeparator = theUri.lastIndexOf('|');
2348                        if (versionSeparator != -1) {
2349                                retVal = theUri.substring(versionSeparator + 1);
2350                        }
2351                }
2352                return retVal;
2353        }
2354
2355        private String getUrlFromIdentifier(String theUri) {
2356                String retVal = theUri;
2357                if (StringUtils.isNotEmpty((theUri))) {
2358                        int versionSeparator = theUri.lastIndexOf('|');
2359                        if (versionSeparator != -1) {
2360                                retVal = theUri.substring(0, versionSeparator);
2361                        }
2362                }
2363                return retVal;
2364        }
2365
2366        @Transactional(propagation = Propagation.REQUIRED, readOnly = true)
2367        @Override
2368        public Set<TermConcept> findCodesAbove(
2369                        Long theCodeSystemResourcePid, Long theCodeSystemVersionPid, String theCode) {
2370                StopWatch stopwatch = new StopWatch();
2371
2372                Optional<TermConcept> concept = fetchLoadedCode(theCodeSystemResourcePid, theCode);
2373                if (concept.isEmpty()) {
2374                        return Collections.emptySet();
2375                }
2376
2377                Set<TermConcept> retVal = new HashSet<>();
2378                retVal.add(concept.get());
2379
2380                fetchParents(concept.get(), retVal);
2381
2382                ourLog.debug("Fetched {} codes above code {} in {}ms", retVal.size(), theCode, stopwatch.getMillis());
2383                return retVal;
2384        }
2385
2386        @Transactional(readOnly = true)
2387        @Override
2388        public List<FhirVersionIndependentConcept> findCodesAbove(String theSystem, String theCode) {
2389                TermCodeSystem cs = getCodeSystem(theSystem);
2390                if (cs == null) {
2391                        return findCodesAboveUsingBuiltInSystems(theSystem, theCode);
2392                }
2393                TermCodeSystemVersion csv = cs.getCurrentVersion();
2394
2395                Set<TermConcept> codes = findCodesAbove(cs.getResource().getId().getId(), csv.getPid(), theCode);
2396                return toVersionIndependentConcepts(theSystem, codes);
2397        }
2398
2399        @Transactional(propagation = Propagation.REQUIRED, readOnly = true)
2400        @Override
2401        public Set<TermConcept> findCodesBelow(
2402                        Long theCodeSystemResourcePid, Long theCodeSystemVersionPid, String theCode) {
2403                Stopwatch stopwatch = Stopwatch.createStarted();
2404
2405                Optional<TermConcept> concept = fetchLoadedCode(theCodeSystemResourcePid, theCode);
2406                if (concept.isEmpty()) {
2407                        return Collections.emptySet();
2408                }
2409
2410                Set<TermConcept> retVal = new HashSet<>();
2411                retVal.add(concept.get());
2412
2413                fetchChildren(concept.get(), retVal);
2414
2415                ourLog.debug(
2416                                "Fetched {} codes below code {} in {}ms",
2417                                retVal.size(),
2418                                theCode,
2419                                stopwatch.elapsed(TimeUnit.MILLISECONDS));
2420                return retVal;
2421        }
2422
2423        @Transactional(readOnly = true)
2424        @Override
2425        public List<FhirVersionIndependentConcept> findCodesBelow(String theSystem, String theCode) {
2426                TermCodeSystem cs = getCodeSystem(theSystem);
2427                if (cs == null) {
2428                        return findCodesBelowUsingBuiltInSystems(theSystem, theCode);
2429                }
2430                TermCodeSystemVersion csv = cs.getCurrentVersion();
2431
2432                Set<TermConcept> codes = findCodesBelow(cs.getResource().getId().getId(), csv.getPid(), theCode);
2433                return toVersionIndependentConcepts(theSystem, codes);
2434        }
2435
2436        private TermCodeSystem getCodeSystem(String theSystem) {
2437                return myCodeSystemDao.findByCodeSystemUri(theSystem);
2438        }
2439
2440        @PostConstruct
2441        public void start() {
2442                RuleBasedTransactionAttribute rules = new RuleBasedTransactionAttribute();
2443                rules.getRollbackRules().add(new NoRollbackRuleAttribute(ExpansionTooCostlyException.class));
2444                myTxTemplate = new TransactionTemplate(myTransactionManager, rules);
2445        }
2446
2447        @Override
2448        public void scheduleJobs(ISchedulerService theSchedulerService) {
2449                // Register scheduled job to pre-expand ValueSets
2450                // In the future it would be great to make this a cluster-aware task somehow
2451                ScheduledJobDefinition vsJobDefinition = new ScheduledJobDefinition();
2452                vsJobDefinition.setId(getClass().getName());
2453                vsJobDefinition.setJobClass(Job.class);
2454                theSchedulerService.scheduleClusteredJob(10 * DateUtils.MILLIS_PER_MINUTE, vsJobDefinition);
2455        }
2456
2457        @Override
2458        public synchronized void preExpandDeferredValueSetsToTerminologyTables() {
2459                if (!myStorageSettings.isEnableTaskPreExpandValueSets()) {
2460                        return;
2461                }
2462                if (isNotSafeToPreExpandValueSets()) {
2463                        ourLog.info("Skipping scheduled pre-expansion of ValueSets while deferred entities are being loaded.");
2464                        return;
2465                }
2466                TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
2467
2468                while (true) {
2469                        StopWatch sw = new StopWatch();
2470                        TermValueSet valueSetToExpand = txTemplate.execute(t -> {
2471                                Optional<TermValueSet> optionalTermValueSet = getNextTermValueSetNotExpanded();
2472                                if (optionalTermValueSet.isEmpty()) {
2473                                        return null;
2474                                }
2475
2476                                TermValueSet termValueSet = optionalTermValueSet.get();
2477                                termValueSet.setTotalConcepts(0L);
2478                                termValueSet.setTotalConceptDesignations(0L);
2479                                termValueSet.setExpansionStatus(TermValueSetPreExpansionStatusEnum.EXPANSION_IN_PROGRESS);
2480                                TermValueSet retVal = myEntityManager.merge(termValueSet);
2481                                myEntityManager.flush();
2482                                return retVal;
2483                        });
2484                        if (valueSetToExpand == null) {
2485                                return;
2486                        }
2487
2488                        // We have a ValueSet to pre-expand.
2489                        setPreExpandingValueSets(true);
2490                        try {
2491                                ValueSet valueSet = txTemplate.execute(t -> {
2492                                        TermValueSet refreshedValueSetToExpand = myTermValueSetDao
2493                                                        .findById(valueSetToExpand.getPartitionedId())
2494                                                        .orElseThrow(() -> new IllegalStateException("Unknown VS ID: " + valueSetToExpand.getId()));
2495                                        return getValueSetFromResourceTable(refreshedValueSetToExpand.getResource());
2496                                });
2497                                assert valueSet != null;
2498
2499                                ValueSetConceptAccumulator valueSetConceptAccumulator =
2500                                                myValueSetConceptAccumulatorFactory.create(valueSetToExpand);
2501                                ValueSetExpansionOptions options = new ValueSetExpansionOptions();
2502                                options.setIncludeHierarchy(true);
2503                                expandValueSet(options, valueSet, valueSetConceptAccumulator);
2504
2505                                // We are done with this ValueSet.
2506                                txTemplate.executeWithoutResult(t -> {
2507                                        valueSetToExpand.setExpansionStatus(TermValueSetPreExpansionStatusEnum.EXPANDED);
2508                                        valueSetToExpand.setExpansionTimestamp(new Date());
2509                                        myEntityManager.merge(valueSetToExpand);
2510                                });
2511
2512                                afterValueSetExpansionStatusChange();
2513
2514                                ourLog.info(
2515                                                "Pre-expanded ValueSet[{}] with URL[{}] - Saved {} concepts in {}",
2516                                                valueSet.getId(),
2517                                                valueSet.getUrl(),
2518                                                valueSetConceptAccumulator.getConceptsSaved(),
2519                                                sw);
2520
2521                        } catch (Exception e) {
2522                                ourLog.error(
2523                                                "Failed to pre-expand ValueSet with URL[{}]: {}", valueSetToExpand.getUrl(), e.getMessage(), e);
2524                                txTemplate.executeWithoutResult(t -> {
2525                                        valueSetToExpand.setExpansionStatus(TermValueSetPreExpansionStatusEnum.FAILED_TO_EXPAND);
2526                                        myEntityManager.merge(valueSetToExpand);
2527                                });
2528
2529                        } finally {
2530                                setPreExpandingValueSets(false);
2531                        }
2532                }
2533        }
2534
2535        /*
2536         * If a ValueSet has just finished pre-expanding, let's flush the caches. This is
2537         * kind of a blunt tool, but it should ensure that users don't get unpredictable
2538         * results while they test changes, which is probably a worthwhile sacrifice
2539         */
2540        private void afterValueSetExpansionStatusChange() {
2541                provideValidationSupport().invalidateCaches();
2542        }
2543
2544        @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2545        @Override
2546        public void invalidateCaches() {
2547                /*
2548                 * Clear out anything left in the userdata caches. We do this mostly because it messes
2549                 * up unit tests to have these things stick around between test runs, since many of
2550                 * these resources come from DefaultProfileValidationSupport and therefore live beyond
2551                 * any single test execution.
2552                 */
2553                for (IBaseResource next : provideValidationSupport().fetchAllConformanceResources()) {
2554                        if (next != null) {
2555                                synchronized (next) {
2556                                        if (next.getUserData(CS_USERDATA_CURRENT_VERSION) != null) {
2557                                                next.setUserData(CS_USERDATA_CURRENT_VERSION, null);
2558                                        }
2559                                        if (next.getUserData(VS_USERDATA_CURRENT_VERSION) != null) {
2560                                                next.setUserData(VS_USERDATA_CURRENT_VERSION, null);
2561                                        }
2562                                }
2563                        }
2564                }
2565        }
2566
2567        private synchronized boolean isPreExpandingValueSets() {
2568                return myPreExpandingValueSets;
2569        }
2570
2571        private synchronized void setPreExpandingValueSets(boolean thePreExpandingValueSets) {
2572                myPreExpandingValueSets = thePreExpandingValueSets;
2573        }
2574
2575        private boolean isNotSafeToPreExpandValueSets() {
2576                return myDeferredStorageSvc != null && !myDeferredStorageSvc.isStorageQueueEmpty(true);
2577        }
2578
2579        private Optional<TermValueSet> getNextTermValueSetNotExpanded() {
2580                Optional<TermValueSet> retVal = Optional.empty();
2581                Slice<TermValueSet> page = myTermValueSetDao.findByExpansionStatus(
2582                                PageRequest.of(0, 1), TermValueSetPreExpansionStatusEnum.NOT_EXPANDED);
2583
2584                if (!page.getContent().isEmpty()) {
2585                        retVal = Optional.of(page.getContent().get(0));
2586                }
2587
2588                return retVal;
2589        }
2590
2591        @Override
2592        @Transactional
2593        public void storeTermValueSet(ResourceTable theResourceTable, ValueSet theValueSet) {
2594                // If we're in a transaction, we need to flush now so that we can correctly detect
2595                // duplicates if there are multiple ValueSets in the same TX with the same URL
2596                // (which is an error, but we need to catch it). It'd be better to catch this by
2597                // inspecting the URLs in the bundle or something, since flushing hurts performance
2598                // but it's not expected that loading valuesets is going to be a huge high frequency
2599                // thing so it probably doesn't matter
2600                myEntityManager.flush();
2601
2602                ValidateUtil.isTrueOrThrowInvalidRequest(theResourceTable != null, "No resource supplied");
2603                if (isPlaceholder(theValueSet)) {
2604                        ourLog.info(
2605                                        "Not storing TermValueSet for placeholder {}",
2606                                        theValueSet.getIdElement().toVersionless().getValueAsString());
2607                        return;
2608                }
2609
2610                ValidateUtil.isNotBlankOrThrowUnprocessableEntity(
2611                                theValueSet.getUrl(), "ValueSet has no value for ValueSet.url");
2612                ourLog.info(
2613                                "Storing TermValueSet for {}",
2614                                theValueSet.getIdElement().toVersionless().getValueAsString());
2615
2616                /*
2617                 * Get CodeSystem and validate CodeSystemVersion
2618                 */
2619                TermValueSet termValueSet = new TermValueSet();
2620                termValueSet.setResource(theResourceTable);
2621                termValueSet.setUrl(theValueSet.getUrl());
2622                termValueSet.setVersion(theValueSet.getVersion());
2623                termValueSet.setName(theValueSet.hasName() ? theValueSet.getName() : null);
2624
2625                // Delete version being replaced
2626                Optional<TermValueSet> deletedTrmValueSet = deleteValueSetForResource(theResourceTable);
2627
2628                /*
2629                 * Do the upload.
2630                 */
2631                String url = termValueSet.getUrl();
2632                String version = termValueSet.getVersion();
2633                Optional<TermValueSet> optionalExistingTermValueSetByUrl;
2634
2635                if (deletedTrmValueSet.isPresent()
2636                                && Objects.equals(deletedTrmValueSet.get().getUrl(), url)
2637                                && Objects.equals(deletedTrmValueSet.get().getVersion(), version)) {
2638                        // If we just deleted the valueset marker, we don't need to check if it exists
2639                        // in the database
2640                        optionalExistingTermValueSetByUrl = Optional.empty();
2641                } else {
2642                        optionalExistingTermValueSetByUrl = getTermValueSet(version, url);
2643                }
2644
2645                if (optionalExistingTermValueSetByUrl.isEmpty()) {
2646
2647                        myEntityManager.persist(termValueSet);
2648
2649                } else {
2650                        TermValueSet existingTermValueSet = optionalExistingTermValueSetByUrl.get();
2651                        String msg;
2652                        if (version != null) {
2653                                msg = myContext
2654                                                .getLocalizer()
2655                                                .getMessage(
2656                                                                TermReadSvcImpl.class,
2657                                                                "cannotCreateDuplicateValueSetUrlAndVersion",
2658                                                                url,
2659                                                                version,
2660                                                                existingTermValueSet
2661                                                                                .getResource()
2662                                                                                .getIdDt()
2663                                                                                .toUnqualifiedVersionless()
2664                                                                                .getValue());
2665                        } else {
2666                                msg = myContext
2667                                                .getLocalizer()
2668                                                .getMessage(
2669                                                                TermReadSvcImpl.class,
2670                                                                "cannotCreateDuplicateValueSetUrl",
2671                                                                url,
2672                                                                existingTermValueSet
2673                                                                                .getResource()
2674                                                                                .getIdDt()
2675                                                                                .toUnqualifiedVersionless()
2676                                                                                .getValue());
2677                        }
2678                        throw new UnprocessableEntityException(Msg.code(902) + msg);
2679                }
2680        }
2681
2682        private Optional<TermValueSet> getTermValueSet(String version, String url) {
2683                Optional<TermValueSet> optionalExistingTermValueSetByUrl;
2684                if (version != null) {
2685                        optionalExistingTermValueSetByUrl = myTermValueSetDao.findTermValueSetByUrlAndVersion(url, version);
2686                } else {
2687                        optionalExistingTermValueSetByUrl = myTermValueSetDao.findTermValueSetByUrlAndNullVersion(url);
2688                }
2689                return optionalExistingTermValueSetByUrl;
2690        }
2691
2692        @Override
2693        @Transactional
2694        public IFhirResourceDaoCodeSystem.SubsumesResult subsumes(
2695                        IPrimitiveType<String> theCodeA,
2696                        IPrimitiveType<String> theCodeB,
2697                        IPrimitiveType<String> theSystem,
2698                        IBaseCoding theCodingA,
2699                        IBaseCoding theCodingB) {
2700                FhirVersionIndependentConcept conceptA = toConcept(theCodeA, theSystem, theCodingA);
2701                FhirVersionIndependentConcept conceptB = toConcept(theCodeB, theSystem, theCodingB);
2702
2703                if (!StringUtils.equals(conceptA.getSystem(), conceptB.getSystem())) {
2704                        throw new InvalidRequestException(
2705                                        Msg.code(903) + "Unable to test subsumption across different code systems");
2706                }
2707
2708                if (!StringUtils.equals(conceptA.getSystemVersion(), conceptB.getSystemVersion())) {
2709                        throw new InvalidRequestException(
2710                                        Msg.code(904) + "Unable to test subsumption across different code system versions");
2711                }
2712
2713                String codeASystemIdentifier;
2714                if (StringUtils.isNotEmpty(conceptA.getSystemVersion())) {
2715                        codeASystemIdentifier = conceptA.getSystem() + OUR_PIPE_CHARACTER + conceptA.getSystemVersion();
2716                } else {
2717                        codeASystemIdentifier = conceptA.getSystem();
2718                }
2719                TermConcept codeA = findCode(codeASystemIdentifier, conceptA.getCode())
2720                                .orElseThrow(() -> new InvalidRequestException("Unknown code: " + conceptA));
2721
2722                String codeBSystemIdentifier;
2723                if (StringUtils.isNotEmpty(conceptB.getSystemVersion())) {
2724                        codeBSystemIdentifier = conceptB.getSystem() + OUR_PIPE_CHARACTER + conceptB.getSystemVersion();
2725                } else {
2726                        codeBSystemIdentifier = conceptB.getSystem();
2727                }
2728                TermConcept codeB = findCode(codeBSystemIdentifier, conceptB.getCode())
2729                                .orElseThrow(() -> new InvalidRequestException("Unknown code: " + conceptB));
2730
2731                SearchSession searchSession = Search.session(myEntityManager);
2732
2733                ConceptSubsumptionOutcome subsumes;
2734                subsumes = testForSubsumption(searchSession, codeA, codeB, ConceptSubsumptionOutcome.SUBSUMES);
2735                if (subsumes == null) {
2736                        subsumes = testForSubsumption(searchSession, codeB, codeA, ConceptSubsumptionOutcome.SUBSUMEDBY);
2737                }
2738                if (subsumes == null) {
2739                        subsumes = ConceptSubsumptionOutcome.NOTSUBSUMED;
2740                }
2741
2742                return new IFhirResourceDaoCodeSystem.SubsumesResult(subsumes);
2743        }
2744
2745        @Override
2746        public IValidationSupport.LookupCodeResult lookupCode(
2747                        ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) {
2748                TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
2749                return txTemplate.execute(t -> {
2750                        final String theSystem = theLookupCodeRequest.getSystem();
2751                        final String theCode = theLookupCodeRequest.getCode();
2752                        Optional<TermConcept> codeOpt = findCode(theSystem, theCode);
2753                        if (codeOpt.isPresent()) {
2754                                TermConcept code = codeOpt.get();
2755
2756                                IValidationSupport.LookupCodeResult result = new IValidationSupport.LookupCodeResult();
2757                                result.setCodeSystemDisplayName(code.getCodeSystemVersion().getCodeSystemDisplayName());
2758                                result.setCodeSystemVersion(code.getCodeSystemVersion().getCodeSystemVersionId());
2759                                result.setSearchedForSystem(theSystem);
2760                                result.setSearchedForCode(theCode);
2761                                result.setFound(true);
2762                                result.setCodeDisplay(code.getDisplay());
2763
2764                                for (TermConceptDesignation next : code.getDesignations()) {
2765                                        // filter out the designation based on displayLanguage if any
2766                                        if (isDisplayLanguageMatch(theLookupCodeRequest.getDisplayLanguage(), next.getLanguage())) {
2767                                                IValidationSupport.ConceptDesignation designation = new IValidationSupport.ConceptDesignation();
2768                                                designation.setLanguage(next.getLanguage());
2769                                                designation.setUseSystem(next.getUseSystem());
2770                                                designation.setUseCode(next.getUseCode());
2771                                                designation.setUseDisplay(next.getUseDisplay());
2772                                                designation.setValue(next.getValue());
2773                                                result.getDesignations().add(designation);
2774                                        }
2775                                }
2776
2777                                final Collection<String> propertyNames = theLookupCodeRequest.getPropertyNames();
2778                                for (TermConceptProperty next : code.getProperties()) {
2779                                        if (ObjectUtils.isNotEmpty(propertyNames) && !propertyNames.contains(next.getKey())) {
2780                                                continue;
2781                                        }
2782                                        if (next.getType() == TermConceptPropertyTypeEnum.CODING) {
2783                                                IValidationSupport.CodingConceptProperty property =
2784                                                                new IValidationSupport.CodingConceptProperty(
2785                                                                                next.getKey(), next.getCodeSystem(), next.getValue(), next.getDisplay());
2786                                                result.getProperties().add(property);
2787                                        } else if (next.getType() == TermConceptPropertyTypeEnum.STRING) {
2788                                                IValidationSupport.StringConceptProperty property =
2789                                                                new IValidationSupport.StringConceptProperty(next.getKey(), next.getValue());
2790                                                result.getProperties().add(property);
2791                                        } else {
2792                                                throw new InternalErrorException(Msg.code(905) + "Unknown type: " + next.getType());
2793                                        }
2794                                }
2795
2796                                return result;
2797
2798                        } else {
2799                                return new LookupCodeResult().setFound(false);
2800                        }
2801                });
2802        }
2803
2804        @Nullable
2805        private ConceptSubsumptionOutcome testForSubsumption(
2806                        SearchSession theSearchSession,
2807                        TermConcept theLeft,
2808                        TermConcept theRight,
2809                        ConceptSubsumptionOutcome theOutput) {
2810                List<TermConcept> fetch = theSearchSession
2811                                .search(TermConcept.class)
2812                                .where(f -> f.bool()
2813                                                .must(f.match().field("myId").matching(theRight.getPid()))
2814                                                .must(f.match().field("myParentPids").matching(Long.toString(theLeft.getId()))))
2815                                .fetchHits(1);
2816
2817                if (fetch.size() > 0) {
2818                        return theOutput;
2819                } else {
2820                        return null;
2821                }
2822        }
2823
2824        private ArrayList<FhirVersionIndependentConcept> toVersionIndependentConcepts(
2825                        String theSystem, Set<TermConcept> codes) {
2826                ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>(codes.size());
2827                for (TermConcept next : codes) {
2828                        retVal.add(new FhirVersionIndependentConcept(theSystem, next.getCode()));
2829                }
2830                return retVal;
2831        }
2832
2833        @Override
2834        @Transactional
2835        public CodeValidationResult validateCodeInValueSet(
2836                        ValidationSupportContext theValidationSupportContext,
2837                        ConceptValidationOptions theOptions,
2838                        String theCodeSystem,
2839                        String theCode,
2840                        String theDisplay,
2841                        @Nonnull IBaseResource theValueSet) {
2842                invokeRunnableForUnitTest();
2843
2844                IPrimitiveType<?> urlPrimitive;
2845                if (theValueSet instanceof org.hl7.fhir.dstu2.model.ValueSet) {
2846                        urlPrimitive = FhirContext.forDstu2Hl7OrgCached()
2847                                        .newTerser()
2848                                        .getSingleValueOrNull(theValueSet, "url", IPrimitiveType.class);
2849                } else {
2850                        urlPrimitive = myContext.newTerser().getSingleValueOrNull(theValueSet, "url", IPrimitiveType.class);
2851                }
2852                String url = urlPrimitive.getValueAsString();
2853                if (isNotBlank(url)) {
2854                        return validateCode(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, url);
2855                }
2856                return null;
2857        }
2858
2859        @CoverageIgnore
2860        @Override
2861        public IValidationSupport.CodeValidationResult validateCode(
2862                        @Nonnull ValidationSupportContext theValidationSupportContext,
2863                        @Nonnull ConceptValidationOptions theOptions,
2864                        String theCodeSystemUrl,
2865                        String theCode,
2866                        String theDisplay,
2867                        String theValueSetUrl) {
2868                // TODO GGG TRY TO JUST AUTO_PASS HERE AND SEE WHAT HAPPENS.
2869                invokeRunnableForUnitTest();
2870                theOptions.setValidateDisplay(isNotBlank(theDisplay));
2871
2872                if (isNotBlank(theValueSetUrl)) {
2873                        return validateCodeInValueSet(
2874                                        theValidationSupportContext, theOptions, theValueSetUrl, theCodeSystemUrl, theCode, theDisplay);
2875                }
2876
2877                TransactionTemplate txTemplate = new TransactionTemplate(myTransactionManager);
2878                txTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
2879                txTemplate.setReadOnly(true);
2880                Optional<FhirVersionIndependentConcept> codeOpt =
2881                                txTemplate.execute(tx -> findCode(theCodeSystemUrl, theCode).map(c -> {
2882                                        String codeSystemVersionId = getCurrentCodeSystemVersion(
2883                                                                        theValidationSupportContext, theCodeSystemUrl)
2884                                                        .myCodeSystemVersionId;
2885                                        return new FhirVersionIndependentConcept(
2886                                                        theCodeSystemUrl, c.getCode(), c.getDisplay(), codeSystemVersionId);
2887                                }));
2888
2889                if (codeOpt != null && codeOpt.isPresent()) {
2890                        FhirVersionIndependentConcept code = codeOpt.get();
2891                        if (!theOptions.isValidateDisplay()
2892                                        || isBlank(code.getDisplay())
2893                                        || isBlank(theDisplay)
2894                                        || code.getDisplay().equals(theDisplay)) {
2895                                return new CodeValidationResult().setCode(code.getCode()).setDisplay(code.getDisplay());
2896                        } else {
2897                                return InMemoryTerminologyServerValidationSupport.createResultForDisplayMismatch(
2898                                                myContext,
2899                                                theCode,
2900                                                theDisplay,
2901                                                code.getDisplay(),
2902                                                code.getSystem(),
2903                                                code.getSystemVersion(),
2904                                                myStorageSettings.getIssueSeverityForCodeDisplayMismatch());
2905                        }
2906                }
2907
2908                return createFailureCodeValidationResult(
2909                                theCodeSystemUrl, theCode, null, createMessageAppendForCodeNotFoundInCodeSystem(theCodeSystemUrl));
2910        }
2911
2912        IValidationSupport.CodeValidationResult validateCodeInValueSet(
2913                        ValidationSupportContext theValidationSupportContext,
2914                        ConceptValidationOptions theValidationOptions,
2915                        String theValueSetUrl,
2916                        String theCodeSystem,
2917                        String theCode,
2918                        String theDisplay) {
2919                IBaseResource valueSet =
2920                                theValidationSupportContext.getRootValidationSupport().fetchValueSet(theValueSetUrl);
2921                CodeValidationResult retVal = null;
2922
2923                // If we don't have a PID, this came from some source other than the JPA
2924                // database, so we don't need to check if it's pre-expanded or not
2925                if (valueSet instanceof IAnyResource) {
2926                        JpaPid pid = IDao.RESOURCE_PID.get(valueSet);
2927                        if (pid != null) {
2928                                TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
2929                                retVal = txTemplate.execute(tx -> {
2930                                        if (isValueSetPreExpandedForCodeValidation(valueSet)) {
2931                                                return validateCodeIsInPreExpandedValueSet(
2932                                                                theValidationSupportContext,
2933                                                                theValidationOptions,
2934                                                                valueSet,
2935                                                                theCodeSystem,
2936                                                                theCode,
2937                                                                theDisplay,
2938                                                                null,
2939                                                                null);
2940                                        } else {
2941                                                return null;
2942                                        }
2943                                });
2944                        }
2945                }
2946
2947                if (retVal == null) {
2948                        if (valueSet != null) {
2949                                retVal = myInMemoryTerminologyServerValidationSupport.validateCodeInValueSet(
2950                                                theValidationSupportContext,
2951                                                theValidationOptions,
2952                                                theCodeSystem,
2953                                                theCode,
2954                                                theDisplay,
2955                                                valueSet);
2956                        } else {
2957                                String append = " - Unable to locate ValueSet[" + theValueSetUrl + "]";
2958                                retVal = createFailureCodeValidationResult(theCodeSystem, theCode, null, append);
2959                        }
2960                }
2961
2962                // Check if someone is accidentally using a VS url where it should be a CS URL
2963                if (retVal != null
2964                                && retVal.getCode() == null
2965                                && theCodeSystem != null
2966                                && myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) {
2967                        if (isValueSetSupported(theValidationSupportContext, theCodeSystem)) {
2968                                if (!isCodeSystemSupported(theValidationSupportContext, theCodeSystem)) {
2969                                        String newMessage = "Unable to validate code " + theCodeSystem + "#" + theCode
2970                                                        + " - Supplied system URL is a ValueSet URL and not a CodeSystem URL, check if it is correct: "
2971                                                        + theCodeSystem;
2972                                        retVal.setMessage(newMessage);
2973                                }
2974                        }
2975                }
2976
2977                return retVal;
2978        }
2979
2980        @Override
2981        public CodeSystem fetchCanonicalCodeSystemFromCompleteContext(String theSystem) {
2982                IValidationSupport validationSupport = provideValidationSupport();
2983                IBaseResource codeSystem = validationSupport.fetchCodeSystem(theSystem);
2984                if (codeSystem != null) {
2985                        codeSystem = myVersionCanonicalizer.codeSystemToCanonical(codeSystem);
2986                }
2987                return (CodeSystem) codeSystem;
2988        }
2989
2990        @Nonnull
2991        private IValidationSupport provideJpaValidationSupport() {
2992                IValidationSupport jpaValidationSupport = myJpaValidationSupport;
2993                if (jpaValidationSupport == null) {
2994                        jpaValidationSupport = myApplicationContext.getBean("myJpaValidationSupport", IValidationSupport.class);
2995                        myJpaValidationSupport = jpaValidationSupport;
2996                }
2997                return jpaValidationSupport;
2998        }
2999
3000        @Nonnull
3001        protected IValidationSupport provideValidationSupport() {
3002                IValidationSupport validationSupport = myValidationSupport;
3003                if (validationSupport == null) {
3004                        validationSupport = myApplicationContext.getBean(IValidationSupport.class);
3005                        myValidationSupport = validationSupport;
3006                }
3007                return validationSupport;
3008        }
3009
3010        public ValueSet fetchCanonicalValueSetFromCompleteContext(String theSystem) {
3011                IValidationSupport validationSupport = provideValidationSupport();
3012                IBaseResource valueSet = validationSupport.fetchValueSet(theSystem);
3013                if (valueSet != null) {
3014                        valueSet = myVersionCanonicalizer.valueSetToCanonical(valueSet);
3015                }
3016                return (ValueSet) valueSet;
3017        }
3018
3019        @Override
3020        public IBaseResource fetchValueSet(String theValueSetUrl) {
3021                return provideJpaValidationSupport().fetchValueSet(theValueSetUrl);
3022        }
3023
3024        @Override
3025        public FhirContext getFhirContext() {
3026                return myContext;
3027        }
3028
3029        private void findCodesAbove(
3030                        CodeSystem theSystem,
3031                        String theSystemString,
3032                        String theCode,
3033                        List<FhirVersionIndependentConcept> theListToPopulate) {
3034                List<CodeSystem.ConceptDefinitionComponent> conceptList = theSystem.getConcept();
3035                for (CodeSystem.ConceptDefinitionComponent next : conceptList) {
3036                        addTreeIfItContainsCode(theSystemString, next, theCode, theListToPopulate);
3037                }
3038        }
3039
3040        @Override
3041        public List<FhirVersionIndependentConcept> findCodesAboveUsingBuiltInSystems(String theSystem, String theCode) {
3042                ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>();
3043                CodeSystem system = fetchCanonicalCodeSystemFromCompleteContext(theSystem);
3044                if (system != null) {
3045                        findCodesAbove(system, theSystem, theCode, retVal);
3046                }
3047                return retVal;
3048        }
3049
3050        private void findCodesBelow(
3051                        CodeSystem theSystem,
3052                        String theSystemString,
3053                        String theCode,
3054                        List<FhirVersionIndependentConcept> theListToPopulate) {
3055                List<CodeSystem.ConceptDefinitionComponent> conceptList = theSystem.getConcept();
3056                findCodesBelow(theSystemString, theCode, theListToPopulate, conceptList);
3057        }
3058
3059        private void findCodesBelow(
3060                        String theSystemString,
3061                        String theCode,
3062                        List<FhirVersionIndependentConcept> theListToPopulate,
3063                        List<CodeSystem.ConceptDefinitionComponent> conceptList) {
3064                for (CodeSystem.ConceptDefinitionComponent next : conceptList) {
3065                        if (theCode.equals(next.getCode())) {
3066                                addAllChildren(theSystemString, next, theListToPopulate);
3067                        } else {
3068                                findCodesBelow(theSystemString, theCode, theListToPopulate, next.getConcept());
3069                        }
3070                }
3071        }
3072
3073        @Override
3074        public List<FhirVersionIndependentConcept> findCodesBelowUsingBuiltInSystems(String theSystem, String theCode) {
3075                ArrayList<FhirVersionIndependentConcept> retVal = new ArrayList<>();
3076                CodeSystem system = fetchCanonicalCodeSystemFromCompleteContext(theSystem);
3077                if (system != null) {
3078                        findCodesBelow(system, theSystem, theCode, retVal);
3079                }
3080                return retVal;
3081        }
3082
3083        private void addAllChildren(
3084                        String theSystemString,
3085                        CodeSystem.ConceptDefinitionComponent theCode,
3086                        List<FhirVersionIndependentConcept> theListToPopulate) {
3087                if (isNotBlank(theCode.getCode())) {
3088                        theListToPopulate.add(new FhirVersionIndependentConcept(theSystemString, theCode.getCode()));
3089                }
3090                for (CodeSystem.ConceptDefinitionComponent nextChild : theCode.getConcept()) {
3091                        addAllChildren(theSystemString, nextChild, theListToPopulate);
3092                }
3093        }
3094
3095        private boolean addTreeIfItContainsCode(
3096                        String theSystemString,
3097                        CodeSystem.ConceptDefinitionComponent theNext,
3098                        String theCode,
3099                        List<FhirVersionIndependentConcept> theListToPopulate) {
3100                boolean foundCodeInChild = false;
3101                for (CodeSystem.ConceptDefinitionComponent nextChild : theNext.getConcept()) {
3102                        foundCodeInChild |= addTreeIfItContainsCode(theSystemString, nextChild, theCode, theListToPopulate);
3103                }
3104
3105                if (theCode.equals(theNext.getCode()) || foundCodeInChild) {
3106                        theListToPopulate.add(new FhirVersionIndependentConcept(theSystemString, theNext.getCode()));
3107                        return true;
3108                }
3109
3110                return false;
3111        }
3112
3113        @Nonnull
3114        private FhirVersionIndependentConcept toConcept(
3115                        IPrimitiveType<String> theCodeType,
3116                        IPrimitiveType<String> theCodeSystemIdentifierType,
3117                        IBaseCoding theCodingType) {
3118                String code = theCodeType != null ? theCodeType.getValueAsString() : null;
3119                String system = theCodeSystemIdentifierType != null
3120                                ? getUrlFromIdentifier(theCodeSystemIdentifierType.getValueAsString())
3121                                : null;
3122                String systemVersion = theCodeSystemIdentifierType != null
3123                                ? getVersionFromIdentifier(theCodeSystemIdentifierType.getValueAsString())
3124                                : null;
3125                if (theCodingType != null) {
3126                        Coding canonicalizedCoding = myVersionCanonicalizer.codingToCanonical(theCodingType);
3127                        assert canonicalizedCoding != null; // Shouldn't be null, since theCodingType isn't
3128                        code = canonicalizedCoding.getCode();
3129                        system = canonicalizedCoding.getSystem();
3130                        systemVersion = canonicalizedCoding.getVersion();
3131                }
3132                return new FhirVersionIndependentConcept(system, code, null, systemVersion);
3133        }
3134
3135        /**
3136         * When the search is for unversioned loinc system it uses the forcedId to obtain the current
3137         * version, as it is not necessarily the last  one anymore.
3138         * For other cases it keeps on considering the last uploaded as the current
3139         */
3140        @Override
3141        public Optional<TermValueSet> findCurrentTermValueSet(String theUrl) {
3142                if (TermReadSvcUtil.isLoincUnversionedValueSet(theUrl)) {
3143                        Optional<String> vsIdOpt = TermReadSvcUtil.getValueSetId(theUrl);
3144                        if (vsIdOpt.isEmpty()) {
3145                                return Optional.empty();
3146                        }
3147
3148                        return myTermValueSetDao.findTermValueSetByForcedId(vsIdOpt.get());
3149                }
3150
3151                List<TermValueSet> termValueSetList = myTermValueSetDao.findTermValueSetByUrl(Pageable.ofSize(1), theUrl);
3152                if (termValueSetList.isEmpty()) {
3153                        return Optional.empty();
3154                }
3155
3156                return Optional.of(termValueSetList.get(0));
3157        }
3158
3159        @Override
3160        public Optional<IBaseResource> readCodeSystemByForcedId(String theForcedId) {
3161                @SuppressWarnings("unchecked")
3162                List<ResourceTable> resultList = (List<ResourceTable>) myEntityManager
3163                                .createQuery("select r from ResourceTable r "
3164                                                + "where r.myResourceType = 'CodeSystem' and r.myFhirId = :fhirId")
3165                                .setParameter("fhirId", theForcedId)
3166                                .getResultList();
3167                if (resultList.isEmpty()) return Optional.empty();
3168
3169                if (resultList.size() > 1)
3170                        throw new NonUniqueResultException(Msg.code(911) + "More than one CodeSystem is pointed by forcedId: "
3171                                        + theForcedId + ". Was constraint " + ResourceTable.IDX_RES_TYPE_FHIR_ID + " removed?");
3172
3173                IFhirResourceDao<CodeSystem> csDao = myDaoRegistry.getResourceDao("CodeSystem");
3174                IBaseResource cs = myJpaStorageResourceParser.toResource(resultList.get(0), false);
3175                return Optional.of(cs);
3176        }
3177
3178        @Transactional
3179        @Override
3180        public ReindexTerminologyResult reindexTerminology() throws InterruptedException {
3181                if (myFulltextSearchSvc == null) {
3182                        return ReindexTerminologyResult.SEARCH_SVC_DISABLED;
3183                }
3184
3185                if (isBatchTerminologyTasksRunning()) {
3186                        return ReindexTerminologyResult.OTHER_BATCH_TERMINOLOGY_TASKS_RUNNING;
3187                }
3188
3189                // disallow pre-expanding ValueSets while reindexing
3190                myDeferredStorageSvc.setProcessDeferred(false);
3191
3192                int objectLoadingThreadNumber = calculateObjectLoadingThreadNumber();
3193                ourLog.info("Using {} threads to load objects", objectLoadingThreadNumber);
3194
3195                try {
3196                        SearchSession searchSession = getSearchSession();
3197                        searchSession
3198                                        .massIndexer(TermConcept.class)
3199                                        .dropAndCreateSchemaOnStart(true)
3200                                        .purgeAllOnStart(false)
3201                                        .batchSizeToLoadObjects(100)
3202                                        .cacheMode(CacheMode.IGNORE)
3203                                        .threadsToLoadObjects(6)
3204                                        .transactionTimeout(60 * SECONDS_IN_MINUTE)
3205                                        .monitor(new PojoMassIndexingLoggingMonitor(INDEXED_ROOTS_LOGGING_COUNT))
3206                                        .startAndWait();
3207                } finally {
3208                        myDeferredStorageSvc.setProcessDeferred(true);
3209                }
3210
3211                return ReindexTerminologyResult.SUCCESS;
3212        }
3213
3214        @VisibleForTesting
3215        boolean isBatchTerminologyTasksRunning() {
3216                return isNotSafeToPreExpandValueSets() || isPreExpandingValueSets();
3217        }
3218
3219        @VisibleForTesting
3220        int calculateObjectLoadingThreadNumber() {
3221                IConnectionPoolInfoProvider connectionPoolInfoProvider =
3222                                new ConnectionPoolInfoProvider(myHibernatePropertiesProvider.getDataSource());
3223                Optional<Integer> maxConnectionsOpt = connectionPoolInfoProvider.getTotalConnectionSize();
3224                if (maxConnectionsOpt.isEmpty()) {
3225                        return DEFAULT_MASS_INDEXER_OBJECT_LOADING_THREADS;
3226                }
3227
3228                int maxConnections = maxConnectionsOpt.get();
3229                int usableThreads = maxConnections < 6 ? 1 : maxConnections - 5;
3230                int objectThreads = Math.min(usableThreads, MAX_MASS_INDEXER_OBJECT_LOADING_THREADS);
3231                ourLog.debug(
3232                                "Data source connection pool has {} connections allocated, so reindexing will use {} object "
3233                                                + "loading threads (each using a connection)",
3234                                maxConnections,
3235                                objectThreads);
3236                return objectThreads;
3237        }
3238
3239        @VisibleForTesting
3240        SearchSession getSearchSession() {
3241                return Search.session(myEntityManager);
3242        }
3243
3244        @Override
3245        public ValueSetExpansionOutcome expandValueSet(
3246                        ValidationSupportContext theValidationSupportContext,
3247                        ValueSetExpansionOptions theExpansionOptions,
3248                        @Nonnull IBaseResource theValueSetToExpand) {
3249                ValueSet canonicalInput = myVersionCanonicalizer.valueSetToCanonical(theValueSetToExpand);
3250                org.hl7.fhir.r4.model.ValueSet expandedR4 = expandValueSet(theExpansionOptions, canonicalInput);
3251                return new ValueSetExpansionOutcome(myVersionCanonicalizer.valueSetFromCanonical(expandedR4));
3252        }
3253
3254        @Override
3255        public IBaseResource expandValueSet(ValueSetExpansionOptions theExpansionOptions, IBaseResource theInput) {
3256                org.hl7.fhir.r4.model.ValueSet valueSetToExpand = myVersionCanonicalizer.valueSetToCanonical(theInput);
3257                org.hl7.fhir.r4.model.ValueSet valueSetR4 = expandValueSet(theExpansionOptions, valueSetToExpand);
3258                return myVersionCanonicalizer.valueSetFromCanonical(valueSetR4);
3259        }
3260
3261        @Override
3262        public void expandValueSet(
3263                        ValueSetExpansionOptions theExpansionOptions,
3264                        IBaseResource theValueSetToExpand,
3265                        IValueSetConceptAccumulator theValueSetCodeAccumulator) {
3266                org.hl7.fhir.r4.model.ValueSet valueSetToExpand =
3267                                myVersionCanonicalizer.valueSetToCanonical(theValueSetToExpand);
3268                expandValueSet(theExpansionOptions, valueSetToExpand, theValueSetCodeAccumulator);
3269        }
3270
3271        private org.hl7.fhir.r4.model.ValueSet getValueSetFromResourceTable(ResourceTable theResourceTable) {
3272                Class<? extends IBaseResource> type =
3273                                getFhirContext().getResourceDefinition("ValueSet").getImplementingClass();
3274                IBaseResource valueSet = myJpaStorageResourceParser.toResource(type, theResourceTable, null, false);
3275                return myVersionCanonicalizer.valueSetToCanonical(valueSet);
3276        }
3277
3278        @Override
3279        public CodeValidationResult validateCodeIsInPreExpandedValueSet(
3280                        ValidationSupportContext theValidationSupportContext,
3281                        ConceptValidationOptions theOptions,
3282                        IBaseResource theValueSet,
3283                        String theSystem,
3284                        String theCode,
3285                        String theDisplay,
3286                        IBaseDatatype theCoding,
3287                        IBaseDatatype theCodeableConcept) {
3288                ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSet, "ValueSet must not be null");
3289                org.hl7.fhir.r4.model.ValueSet valueSetR4 = myVersionCanonicalizer.valueSetToCanonical(theValueSet);
3290                org.hl7.fhir.r4.model.Coding codingR4 = myVersionCanonicalizer.codingToCanonical((IBaseCoding) theCoding);
3291                org.hl7.fhir.r4.model.CodeableConcept codeableConcept =
3292                                myVersionCanonicalizer.codeableConceptToCanonical(theCodeableConcept);
3293
3294                return validateCodeIsInPreExpandedValueSet(
3295                                theValidationSupportContext,
3296                                theOptions,
3297                                valueSetR4,
3298                                theSystem,
3299                                theCode,
3300                                theDisplay,
3301                                codingR4,
3302                                codeableConcept);
3303        }
3304
3305        @Override
3306        public boolean isValueSetPreExpandedForCodeValidation(IBaseResource theValueSet) {
3307                ValidateUtil.isNotNullOrThrowUnprocessableEntity(theValueSet, "ValueSet must not be null");
3308                org.hl7.fhir.r4.model.ValueSet valueSetR4 = myVersionCanonicalizer.valueSetToCanonical(theValueSet);
3309                return isValueSetPreExpandedForCodeValidation(valueSetR4);
3310        }
3311
3312        private static class TermCodeSystemVersionDetails {
3313
3314                private final long myPid;
3315                private final String myCodeSystemVersionId;
3316
3317                public TermCodeSystemVersionDetails(long thePid, String theCodeSystemVersionId) {
3318                        myPid = thePid;
3319                        myCodeSystemVersionId = theCodeSystemVersionId;
3320                }
3321        }
3322
3323        public static class Job implements HapiJob {
3324                @Autowired
3325                private ITermReadSvc myTerminologySvc;
3326
3327                @Override
3328                public void execute(JobExecutionContext theContext) {
3329                        myTerminologySvc.preExpandDeferredValueSetsToTerminologyTables();
3330                }
3331        }
3332
3333        /**
3334         * Properties returned from method buildSearchScroll
3335         */
3336        private static final class SearchProperties {
3337                private final List<Supplier<SearchScroll<EntityReference>>> mySearchScroll = new ArrayList<>();
3338                private List<String> myIncludeOrExcludeCodes;
3339
3340                public List<Supplier<SearchScroll<EntityReference>>> getSearchScroll() {
3341                        return mySearchScroll;
3342                }
3343
3344                public void addSearchScroll(Supplier<SearchScroll<EntityReference>> theSearchScrollSupplier) {
3345                        mySearchScroll.add(theSearchScrollSupplier);
3346                }
3347
3348                public List<String> getIncludeOrExcludeCodes() {
3349                        return myIncludeOrExcludeCodes;
3350                }
3351
3352                public void setIncludeOrExcludeCodes(List<String> theIncludeOrExcludeCodes) {
3353                        myIncludeOrExcludeCodes = theIncludeOrExcludeCodes;
3354                }
3355
3356                public boolean hasIncludeOrExcludeCodes() {
3357                        return !myIncludeOrExcludeCodes.isEmpty();
3358                }
3359        }
3360
3361        static boolean isValueSetDisplayLanguageMatch(ValueSetExpansionOptions theExpansionOptions, String theStoredLang) {
3362                if (theExpansionOptions == null) {
3363                        return true;
3364                }
3365
3366                if (theExpansionOptions.getTheDisplayLanguage() == null || theStoredLang == null) {
3367                        return true;
3368                }
3369
3370                return theExpansionOptions.getTheDisplayLanguage().equalsIgnoreCase(theStoredLang);
3371        }
3372
3373        @Nonnull
3374        private static String createMessageAppendForCodeNotFoundInCodeSystem(String theCodeSystemUrl) {
3375                return " - Code is not found in CodeSystem: " + theCodeSystemUrl;
3376        }
3377
3378        @VisibleForTesting
3379        public static void setForceDisableHibernateSearchForUnitTest(boolean theForceDisableHibernateSearchForUnitTest) {
3380                ourForceDisableHibernateSearchForUnitTest = theForceDisableHibernateSearchForUnitTest;
3381        }
3382
3383        static boolean isPlaceholder(DomainResource theResource) {
3384                boolean retVal = false;
3385                Extension extension = theResource.getExtensionByUrl(HapiExtensions.EXT_RESOURCE_PLACEHOLDER);
3386                if (extension != null && extension.hasValue() && extension.getValue() instanceof BooleanType) {
3387                        retVal = ((BooleanType) extension.getValue()).booleanValue();
3388                }
3389                return retVal;
3390        }
3391
3392        /**
3393         * This is only used for unit tests to test failure conditions
3394         */
3395        static void invokeRunnableForUnitTest() {
3396                if (myInvokeOnNextCallForUnitTest != null) {
3397                        Runnable invokeOnNextCallForUnitTest = myInvokeOnNextCallForUnitTest;
3398                        myInvokeOnNextCallForUnitTest = null;
3399                        invokeOnNextCallForUnitTest.run();
3400                }
3401        }
3402
3403        @VisibleForTesting
3404        public static void setInvokeOnNextCallForUnitTest(Runnable theInvokeOnNextCallForUnitTest) {
3405                myInvokeOnNextCallForUnitTest = theInvokeOnNextCallForUnitTest;
3406        }
3407
3408        static List<TermConcept> toPersistedConcepts(
3409                        List<CodeSystem.ConceptDefinitionComponent> theConcept, TermCodeSystemVersion theCodeSystemVersion) {
3410                ArrayList<TermConcept> retVal = new ArrayList<>();
3411
3412                for (CodeSystem.ConceptDefinitionComponent next : theConcept) {
3413                        if (isNotBlank(next.getCode())) {
3414                                TermConcept termConcept = toTermConcept(next, theCodeSystemVersion);
3415                                retVal.add(termConcept);
3416                        }
3417                }
3418
3419                return retVal;
3420        }
3421
3422        @Nonnull
3423        static TermConcept toTermConcept(
3424                        CodeSystem.ConceptDefinitionComponent theConceptDefinition, TermCodeSystemVersion theCodeSystemVersion) {
3425                TermConcept termConcept = new TermConcept();
3426                termConcept.setCode(theConceptDefinition.getCode());
3427                termConcept.setCodeSystemVersion(theCodeSystemVersion);
3428                termConcept.setDisplay(theConceptDefinition.getDisplay());
3429
3430                termConcept.addChildren(
3431                                toPersistedConcepts(theConceptDefinition.getConcept(), theCodeSystemVersion), RelationshipTypeEnum.ISA);
3432
3433                for (CodeSystem.ConceptDefinitionDesignationComponent designationComponent :
3434                                theConceptDefinition.getDesignation()) {
3435                        if (isNotBlank(designationComponent.getValue())) {
3436                                TermConceptDesignation designation = termConcept.addDesignation();
3437                                designation.setLanguage(designationComponent.hasLanguage() ? designationComponent.getLanguage() : null);
3438                                if (designationComponent.hasUse()) {
3439                                        designation.setUseSystem(
3440                                                        designationComponent.getUse().hasSystem()
3441                                                                        ? designationComponent.getUse().getSystem()
3442                                                                        : null);
3443                                        designation.setUseCode(
3444                                                        designationComponent.getUse().hasCode()
3445                                                                        ? designationComponent.getUse().getCode()
3446                                                                        : null);
3447                                        designation.setUseDisplay(
3448                                                        designationComponent.getUse().hasDisplay()
3449                                                                        ? designationComponent.getUse().getDisplay()
3450                                                                        : null);
3451                                }
3452                                designation.setValue(designationComponent.getValue());
3453                        }
3454                }
3455
3456                for (CodeSystem.ConceptPropertyComponent next : theConceptDefinition.getProperty()) {
3457                        TermConceptProperty property = new TermConceptProperty();
3458
3459                        property.setKey(next.getCode());
3460                        property.setConcept(termConcept);
3461                        property.setCodeSystemVersion(theCodeSystemVersion);
3462
3463                        if (next.getValue() instanceof StringType) {
3464                                property.setType(TermConceptPropertyTypeEnum.STRING);
3465                                property.setValue(next.getValueStringType().getValue());
3466                        } else if (next.getValue() instanceof BooleanType) {
3467                                property.setType(TermConceptPropertyTypeEnum.BOOLEAN);
3468                                property.setValue(((BooleanType) next.getValue()).getValueAsString());
3469                        } else if (next.getValue() instanceof IntegerType) {
3470                                property.setType(TermConceptPropertyTypeEnum.INTEGER);
3471                                property.setValue(((IntegerType) next.getValue()).getValueAsString());
3472                        } else if (next.getValue() instanceof DecimalType) {
3473                                property.setType(TermConceptPropertyTypeEnum.DECIMAL);
3474                                property.setValue(((DecimalType) next.getValue()).getValueAsString());
3475                        } else if (next.getValue() instanceof DateTimeType) {
3476                                // DateType is not supported because it's not
3477                                // supported in CodeSystem.setValue
3478                                property.setType(TermConceptPropertyTypeEnum.DATETIME);
3479                                property.setValue(((DateTimeType) next.getValue()).getValueAsString());
3480                        } else if (next.getValue() instanceof Coding) {
3481                                Coding nextCoding = next.getValueCoding();
3482                                property.setType(TermConceptPropertyTypeEnum.CODING);
3483                                property.setCodeSystem(nextCoding.getSystem());
3484                                property.setValue(nextCoding.getCode());
3485                                property.setDisplay(nextCoding.getDisplay());
3486                        } else if (next.getValue() != null) {
3487                                ourLog.warn("Don't know how to handle properties of type: "
3488                                                + next.getValue().getClass());
3489                                continue;
3490                        }
3491
3492                        termConcept.getProperties().add(property);
3493                }
3494                return termConcept;
3495        }
3496
3497        static boolean isDisplayLanguageMatch(String theReqLang, String theStoredLang) {
3498                // NOTE: return the designation when one of then is not specified.
3499                if (theReqLang == null || theStoredLang == null) return true;
3500
3501                return theReqLang.equalsIgnoreCase(theStoredLang);
3502        }
3503}