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