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