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