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.dao;
021
022import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
024import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
025import ca.uhn.fhir.context.FhirContext;
026import ca.uhn.fhir.context.FhirVersionEnum;
027import ca.uhn.fhir.context.RuntimeChildResourceDefinition;
028import ca.uhn.fhir.context.RuntimeResourceDefinition;
029import ca.uhn.fhir.i18n.Msg;
030import ca.uhn.fhir.interceptor.api.HookParams;
031import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
032import ca.uhn.fhir.interceptor.api.Pointcut;
033import ca.uhn.fhir.interceptor.model.RequestPartitionId;
034import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
035import ca.uhn.fhir.jpa.api.dao.IDao;
036import ca.uhn.fhir.jpa.api.dao.IJpaDao;
037import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
038import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
039import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
040import ca.uhn.fhir.jpa.cache.IResourceTypeCacheSvc;
041import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
042import ca.uhn.fhir.jpa.dao.data.IResourceLinkDao;
043import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
044import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
045import ca.uhn.fhir.jpa.dao.expunge.ExpungeService;
046import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer;
047import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor;
048import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
049import ca.uhn.fhir.jpa.delete.DeleteConflictService;
050import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceAddress;
051import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceAddressMetadataKey;
052import ca.uhn.fhir.jpa.esr.ExternallyStoredResourceServiceRegistry;
053import ca.uhn.fhir.jpa.model.config.PartitionSettings;
054import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
055import ca.uhn.fhir.jpa.model.cross.IResourceLookup;
056import ca.uhn.fhir.jpa.model.dao.JpaPid;
057import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
058import ca.uhn.fhir.jpa.model.entity.BaseTag;
059import ca.uhn.fhir.jpa.model.entity.EntityIndexStatusEnum;
060import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
061import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
062import ca.uhn.fhir.jpa.model.entity.ResourceLink;
063import ca.uhn.fhir.jpa.model.entity.ResourceTable;
064import ca.uhn.fhir.jpa.model.entity.ResourceTag;
065import ca.uhn.fhir.jpa.model.entity.TagDefinition;
066import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
067import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData;
068import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
069import ca.uhn.fhir.jpa.model.util.JpaConstants;
070import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc;
071import ca.uhn.fhir.jpa.searchparam.extractor.LogicalReferenceHelper;
072import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
073import ca.uhn.fhir.jpa.searchparam.fulltext.FullTextExtractionRequest;
074import ca.uhn.fhir.jpa.searchparam.fulltext.FullTextExtractionResponse;
075import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
076import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
077import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
078import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
079import ca.uhn.fhir.jpa.util.AddRemoveCount;
080import ca.uhn.fhir.jpa.util.QueryChunker;
081import ca.uhn.fhir.model.api.IResource;
082import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
083import ca.uhn.fhir.model.api.Tag;
084import ca.uhn.fhir.model.api.TagList;
085import ca.uhn.fhir.model.base.composite.BaseCodingDt;
086import ca.uhn.fhir.model.primitive.IdDt;
087import ca.uhn.fhir.parser.DataFormatException;
088import ca.uhn.fhir.rest.api.Constants;
089import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum;
090import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
091import ca.uhn.fhir.rest.api.server.RequestDetails;
092import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
093import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
094import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
095import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
096import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
097import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
098import ca.uhn.fhir.util.CoverageIgnore;
099import ca.uhn.fhir.util.HapiExtensions;
100import ca.uhn.fhir.util.MetaUtil;
101import ca.uhn.fhir.util.StopWatch;
102import ca.uhn.fhir.util.XmlUtil;
103import com.google.common.annotations.VisibleForTesting;
104import com.google.common.base.Charsets;
105import com.google.common.collect.Sets;
106import com.google.common.hash.HashCode;
107import jakarta.annotation.Nonnull;
108import jakarta.annotation.Nullable;
109import jakarta.annotation.PostConstruct;
110import jakarta.persistence.EntityManager;
111import jakarta.persistence.PersistenceContext;
112import jakarta.persistence.PersistenceContextType;
113import org.apache.commons.collections4.CollectionUtils;
114import org.apache.commons.lang3.NotImplementedException;
115import org.apache.commons.lang3.StringUtils;
116import org.apache.commons.lang3.Validate;
117import org.hl7.fhir.instance.model.api.IAnyResource;
118import org.hl7.fhir.instance.model.api.IBase;
119import org.hl7.fhir.instance.model.api.IBaseCoding;
120import org.hl7.fhir.instance.model.api.IBaseExtension;
121import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
122import org.hl7.fhir.instance.model.api.IBaseMetaType;
123import org.hl7.fhir.instance.model.api.IBaseReference;
124import org.hl7.fhir.instance.model.api.IBaseResource;
125import org.hl7.fhir.instance.model.api.IDomainResource;
126import org.hl7.fhir.instance.model.api.IIdType;
127import org.hl7.fhir.instance.model.api.IPrimitiveType;
128import org.slf4j.Logger;
129import org.slf4j.LoggerFactory;
130import org.springframework.beans.BeansException;
131import org.springframework.beans.factory.annotation.Autowired;
132import org.springframework.context.ApplicationContext;
133import org.springframework.context.ApplicationContextAware;
134import org.springframework.stereotype.Repository;
135
136import java.util.ArrayList;
137import java.util.Collection;
138import java.util.Collections;
139import java.util.Date;
140import java.util.HashSet;
141import java.util.IdentityHashMap;
142import java.util.List;
143import java.util.Set;
144import java.util.StringTokenizer;
145import java.util.function.Consumer;
146import java.util.function.Supplier;
147import java.util.stream.Collectors;
148import javax.xml.stream.events.Characters;
149import javax.xml.stream.events.XMLEvent;
150
151import static java.util.Objects.nonNull;
152import static org.apache.commons.collections4.CollectionUtils.isEqualCollection;
153import static org.apache.commons.lang3.StringUtils.isBlank;
154import static org.apache.commons.lang3.StringUtils.isNotBlank;
155import static org.apache.commons.lang3.StringUtils.left;
156import static org.apache.commons.lang3.StringUtils.trim;
157
158/**
159 * TODO: JA - This class has only one subclass now. Historically it was a common
160 * ancestor for BaseHapiFhirSystemDao and BaseHapiFhirResourceDao but I've untangled
161 * the former from this hierarchy in order to simplify moving common functionality
162 * for resource DAOs into the hapi-fhir-storage project. This class should be merged
163 * into BaseHapiFhirResourceDao, but that should be done in its own dedicated PR
164 * since it'll be a noisy change.
165 */
166@SuppressWarnings("WeakerAccess")
167@Repository
168public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStorageResourceDao<T>
169                implements IDao, IJpaDao<T>, ApplicationContextAware {
170
171        public static final String NS_JPA_PROFILE = "https://github.com/hapifhir/hapi-fhir/ns/jpa/profile";
172        private static final Logger ourLog = LoggerFactory.getLogger(BaseHapiFhirDao.class);
173        private static boolean ourValidationDisabledForUnitTest;
174        private static boolean ourDisableIncrementOnUpdateForUnitTest = false;
175
176        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
177        protected EntityManager myEntityManager;
178
179        @Autowired
180        protected IIdHelperService<JpaPid> myIdHelperService;
181
182        @Autowired
183        protected ISearchCoordinatorSvc<JpaPid> mySearchCoordinatorSvc;
184
185        @Autowired
186        protected ITermReadSvc myTerminologySvc;
187
188        @Autowired
189        protected IResourceHistoryTableDao myResourceHistoryTableDao;
190
191        @Autowired
192        protected IResourceTableDao myResourceTableDao;
193
194        @Autowired
195        protected IResourceLinkDao myResourceLinkDao;
196
197        @Autowired
198        protected IResourceTagDao myResourceTagDao;
199
200        @Autowired
201        protected DeleteConflictService myDeleteConflictService;
202
203        @Autowired
204        protected IInterceptorBroadcaster myInterceptorBroadcaster;
205
206        @Autowired
207        protected InMemoryResourceMatcher myInMemoryResourceMatcher;
208
209        @Autowired
210        protected IJpaStorageResourceParser myJpaStorageResourceParser;
211
212        @Autowired
213        protected PartitionSettings myPartitionSettings;
214
215        @Autowired
216        ExpungeService myExpungeService;
217
218        @Autowired
219        private ExternallyStoredResourceServiceRegistry myExternallyStoredResourceServiceRegistry;
220
221        @Autowired
222        private ISearchParamPresenceSvc mySearchParamPresenceSvc;
223
224        @Autowired
225        private SearchParamWithInlineReferencesExtractor mySearchParamWithInlineReferencesExtractor;
226
227        @Autowired
228        private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer;
229
230        private FhirContext myContext;
231        private ApplicationContext myApplicationContext;
232
233        @Autowired
234        private IPartitionLookupSvc myPartitionLookupSvc;
235
236        @Autowired(required = false)
237        private IFulltextSearchSvc myFulltextSearchSvc;
238
239        @Autowired
240        protected ResourceHistoryCalculator myResourceHistoryCalculator;
241
242        @Autowired
243        protected CacheTagDefinitionDao cacheTagDefinitionDao;
244
245        @Autowired
246        protected IResourceTypeCacheSvc myResourceTypeCacheSvc;
247
248        protected final CodingSpy myCodingSpy = new CodingSpy();
249
250        @VisibleForTesting
251        public void setExternallyStoredResourceServiceRegistryForUnitTest(
252                        ExternallyStoredResourceServiceRegistry theExternallyStoredResourceServiceRegistry) {
253                myExternallyStoredResourceServiceRegistry = theExternallyStoredResourceServiceRegistry;
254        }
255
256        @VisibleForTesting
257        public void setSearchParamPresenceSvc(ISearchParamPresenceSvc theSearchParamPresenceSvc) {
258                mySearchParamPresenceSvc = theSearchParamPresenceSvc;
259        }
260
261        @VisibleForTesting
262        public void setResourceHistoryCalculator(ResourceHistoryCalculator theResourceHistoryCalculator) {
263                myResourceHistoryCalculator = theResourceHistoryCalculator;
264        }
265
266        @Override
267        protected IInterceptorBroadcaster getInterceptorBroadcaster() {
268                return myInterceptorBroadcaster;
269        }
270
271        protected ApplicationContext getApplicationContext() {
272                return myApplicationContext;
273        }
274
275        @Override
276        public void setApplicationContext(@Nonnull ApplicationContext theApplicationContext) throws BeansException {
277                /*
278                 * We do a null check here because Smile's module system tries to
279                 * initialize the application context twice if two modules depend on
280                 * the persistence module. The second time sets the dependency's appctx.
281                 */
282                if (myApplicationContext == null) {
283                        myApplicationContext = theApplicationContext;
284                }
285        }
286
287        private void extractHapiTags(
288                        TransactionDetails theTransactionDetails,
289                        IResource theResource,
290                        ResourceTable theEntity,
291                        Set<ResourceTag> allDefs) {
292                TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(theResource);
293                if (tagList != null) {
294                        for (Tag next : tagList) {
295                                TagDefinition def = cacheTagDefinitionDao.getTagOrNull(
296                                                theTransactionDetails,
297                                                TagTypeEnum.TAG,
298                                                next.getScheme(),
299                                                next.getTerm(),
300                                                next.getLabel(),
301                                                next.getVersion(),
302                                                myCodingSpy.getBooleanObject(next));
303                                if (def != null) {
304                                        ResourceTag tag = theEntity.addTag(def);
305                                        allDefs.add(tag);
306                                        theEntity.setHasTags(true);
307                                }
308                        }
309                }
310
311                List<BaseCodingDt> securityLabels = ResourceMetadataKeyEnum.SECURITY_LABELS.get(theResource);
312                if (securityLabels != null) {
313                        for (BaseCodingDt next : securityLabels) {
314                                TagDefinition def = cacheTagDefinitionDao.getTagOrNull(
315                                                theTransactionDetails,
316                                                TagTypeEnum.SECURITY_LABEL,
317                                                next.getSystemElement().getValue(),
318                                                next.getCodeElement().getValue(),
319                                                next.getDisplayElement().getValue(),
320                                                next.getVersionElement().getValue(),
321                                                next.getUserSelectedElement().getValue());
322                                if (def != null) {
323                                        ResourceTag tag = theEntity.addTag(def);
324                                        allDefs.add(tag);
325                                        theEntity.setHasTags(true);
326                                }
327                        }
328                }
329
330                List<IdDt> profiles = ResourceMetadataKeyEnum.PROFILES.get(theResource);
331                if (profiles != null) {
332                        for (IIdType next : profiles) {
333                                TagDefinition def = cacheTagDefinitionDao.getTagOrNull(
334                                                theTransactionDetails, TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null, null, null);
335                                if (def != null) {
336                                        ResourceTag tag = theEntity.addTag(def);
337                                        allDefs.add(tag);
338                                        theEntity.setHasTags(true);
339                                }
340                        }
341                }
342        }
343
344        private void extractRiTags(
345                        TransactionDetails theTransactionDetails,
346                        IAnyResource theResource,
347                        ResourceTable theEntity,
348                        Set<ResourceTag> theAllTags) {
349                List<? extends IBaseCoding> tagList = theResource.getMeta().getTag();
350                if (tagList != null) {
351                        for (IBaseCoding next : tagList) {
352                                TagDefinition def = cacheTagDefinitionDao.getTagOrNull(
353                                                theTransactionDetails,
354                                                TagTypeEnum.TAG,
355                                                next.getSystem(),
356                                                next.getCode(),
357                                                next.getDisplay(),
358                                                next.getVersion(),
359                                                myCodingSpy.getBooleanObject(next));
360                                if (def != null) {
361                                        ResourceTag tag = theEntity.addTag(def);
362                                        theAllTags.add(tag);
363                                        theEntity.setHasTags(true);
364                                }
365                        }
366                }
367
368                List<? extends IBaseCoding> securityLabels = theResource.getMeta().getSecurity();
369                if (securityLabels != null) {
370                        for (IBaseCoding next : securityLabels) {
371                                TagDefinition def = cacheTagDefinitionDao.getTagOrNull(
372                                                theTransactionDetails,
373                                                TagTypeEnum.SECURITY_LABEL,
374                                                next.getSystem(),
375                                                next.getCode(),
376                                                next.getDisplay(),
377                                                next.getVersion(),
378                                                myCodingSpy.getBooleanObject(next));
379                                if (def != null) {
380                                        ResourceTag tag = theEntity.addTag(def);
381                                        theAllTags.add(tag);
382                                        theEntity.setHasTags(true);
383                                }
384                        }
385                }
386
387                List<? extends IPrimitiveType<String>> profiles = theResource.getMeta().getProfile();
388                if (profiles != null) {
389                        for (IPrimitiveType<String> next : profiles) {
390                                TagDefinition def = cacheTagDefinitionDao.getTagOrNull(
391                                                theTransactionDetails, TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null, null, null);
392                                if (def != null) {
393                                        ResourceTag tag = theEntity.addTag(def);
394                                        theAllTags.add(tag);
395                                        theEntity.setHasTags(true);
396                                }
397                        }
398                }
399        }
400
401        private void extractProfileTags(
402                        TransactionDetails theTransactionDetails,
403                        IBaseResource theResource,
404                        ResourceTable theEntity,
405                        Set<ResourceTag> theAllTags) {
406                RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource);
407                if (!def.isStandardType()) {
408                        String profile = def.getResourceProfile("");
409                        if (isNotBlank(profile)) {
410                                TagDefinition profileDef = cacheTagDefinitionDao.getTagOrNull(
411                                                theTransactionDetails, TagTypeEnum.PROFILE, NS_JPA_PROFILE, profile, null, null, null);
412
413                                ResourceTag tag = theEntity.addTag(profileDef);
414                                theAllTags.add(tag);
415                                theEntity.setHasTags(true);
416                        }
417                }
418        }
419
420        private Set<ResourceTag> getAllTagDefinitions(ResourceTable theEntity) {
421                HashSet<ResourceTag> retVal = Sets.newHashSet();
422                if (theEntity.isHasTags()) {
423                        retVal.addAll(theEntity.getTags());
424                }
425                return retVal;
426        }
427
428        @Override
429        public JpaStorageSettings getStorageSettings() {
430                return myStorageSettings;
431        }
432
433        @Override
434        public FhirContext getContext() {
435                return myContext;
436        }
437
438        @Autowired
439        public void setContext(FhirContext theContext) {
440                super.myFhirContext = theContext;
441                myContext = theContext;
442        }
443
444        void incrementId(T theResource, ResourceTable theSavedEntity, IIdType theResourceId) {
445                if (theResourceId == null || theResourceId.getVersionIdPart() == null) {
446                        theSavedEntity.initializeVersion();
447                } else {
448                        theSavedEntity.markVersionUpdatedInCurrentTransaction();
449                }
450
451                assert theResourceId != null;
452                String newVersion = Long.toString(theSavedEntity.getVersion());
453                IIdType newId = theResourceId.withVersion(newVersion);
454                theResource.getIdElement().setValue(newId.getValue());
455        }
456
457        public boolean isLogicalReference(IIdType theId) {
458                return LogicalReferenceHelper.isLogicalReference(myStorageSettings, theId);
459        }
460
461        /**
462         * Returns {@literal true} if the resource has changed (either the contents or the tags)
463         */
464        protected EncodedResource populateResourceIntoEntity(
465                        TransactionDetails theTransactionDetails,
466                        RequestDetails theRequest,
467                        IBaseResource theResource,
468                        ResourceTable theEntity,
469                        boolean thePerformIndexing) {
470                if (theEntity.getResourceType() == null) {
471                        theEntity.setResourceType(toResourceName(theResource));
472                }
473                if (theEntity.getResourceTypeId() == null && theResource != null) {
474                        theEntity.setResourceTypeId(myResourceTypeCacheSvc.getResourceTypeId(toResourceName(theResource)));
475                }
476
477                byte[] resourceBinary;
478                String resourceText;
479                ResourceEncodingEnum encoding;
480                boolean changed = false;
481
482                if (theEntity.getDeleted() == null) {
483
484                        if (thePerformIndexing) {
485
486                                ExternallyStoredResourceAddress address = null;
487                                if (myExternallyStoredResourceServiceRegistry.hasProviders()) {
488                                        address = ExternallyStoredResourceAddressMetadataKey.INSTANCE.get(theResource);
489                                }
490
491                                if (address != null) {
492
493                                        encoding = ResourceEncodingEnum.ESR;
494                                        resourceBinary = null;
495                                        resourceText = address.getProviderId() + ":" + address.getLocation();
496                                        changed = true;
497
498                                } else {
499
500                                        encoding = myStorageSettings.getResourceEncoding();
501
502                                        String resourceType = theEntity.getResourceType();
503
504                                        List<String> excludeElements = new ArrayList<>(8);
505                                        IBaseMetaType meta = theResource.getMeta();
506
507                                        IBaseExtension<?, ?> sourceExtension = getExcludedElements(resourceType, excludeElements, meta);
508
509                                        theEntity.setFhirVersion(myContext.getVersion().getVersion());
510
511                                        // TODO:  LD: Once 2024-02 it out the door we should consider further refactoring here to move
512                                        // more of this logic within the calculator and eliminate more local variables
513                                        final ResourceHistoryState calculate = myResourceHistoryCalculator.calculateResourceHistoryState(
514                                                        theResource, encoding, excludeElements);
515
516                                        resourceText = calculate.getResourceText();
517                                        resourceBinary = calculate.getResourceBinary();
518                                        encoding = calculate.getEncoding(); // This may be a no-op
519                                        final HashCode hashCode = calculate.getHashCode();
520
521                                        String hashSha256 = hashCode.toString();
522                                        if (!hashSha256.equals(theEntity.getHashSha256())) {
523                                                changed = true;
524                                        }
525                                        theEntity.setHashSha256(hashSha256);
526
527                                        if (sourceExtension != null) {
528                                                IBaseExtension<?, ?> newSourceExtension = ((IBaseHasExtensions) meta).addExtension();
529                                                newSourceExtension.setUrl(sourceExtension.getUrl());
530                                                newSourceExtension.setValue(sourceExtension.getValue());
531                                        }
532                                }
533
534                        } else {
535
536                                encoding = null;
537                                resourceBinary = null;
538                                resourceText = null;
539                        }
540
541                        boolean skipUpdatingTags = myStorageSettings.isMassIngestionMode() && theEntity.isHasTags();
542                        skipUpdatingTags |= myStorageSettings.getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.INLINE;
543
544                        if (!skipUpdatingTags) {
545                                changed |= updateTags(theTransactionDetails, theRequest, theResource, theEntity);
546                        }
547
548                } else {
549
550                        if (nonNull(theEntity.getHashSha256())) {
551                                theEntity.setHashSha256(null);
552                                changed = true;
553                        }
554
555                        resourceBinary = null;
556                        resourceText = null;
557                        encoding = ResourceEncodingEnum.DEL;
558                }
559
560                if (thePerformIndexing && !changed) {
561                        if (theEntity.getId() == null) {
562                                changed = true;
563                        } else if (myStorageSettings.isMassIngestionMode()) {
564
565                                // Don't check existing - We'll rely on the SHA256 hash only
566
567                        } else if (theEntity.getVersion() == 1L && theEntity.getCurrentVersionEntity() == null) {
568
569                                // No previous version if this is the first version
570
571                        } else {
572                                ResourceHistoryTable currentHistoryVersion = theEntity.getCurrentVersionEntity();
573                                if (currentHistoryVersion == null) {
574                                        currentHistoryVersion = myResourceHistoryTableDao.findForIdAndVersion(
575                                                        theEntity.getId().toFk(), theEntity.getVersion());
576                                }
577                                if (currentHistoryVersion == null || !currentHistoryVersion.hasResource()) {
578                                        changed = true;
579                                } else {
580                                        // TODO:  LD: Once 2024-02 it out the door we should consider further refactoring here to move
581                                        // more of this logic within the calculator and eliminate more local variables
582                                        changed = myResourceHistoryCalculator.isResourceHistoryChanged(
583                                                        currentHistoryVersion, resourceBinary, resourceText);
584                                }
585                        }
586                }
587
588                EncodedResource retVal = new EncodedResource();
589                retVal.setEncoding(encoding);
590                retVal.setResourceBinary(resourceBinary);
591                retVal.setResourceText(resourceText);
592                retVal.setChanged(changed);
593
594                return retVal;
595        }
596
597        /**
598         * helper to format the meta element for serialization of the resource.
599         *
600         * @param theResourceType    the resource type of the resource
601         * @param theExcludeElements list of extensions in the meta element to exclude from serialization
602         * @param theMeta            the meta element of the resource
603         * @return source extension if present in the meta element
604         */
605        private IBaseExtension<?, ?> getExcludedElements(
606                        String theResourceType, List<String> theExcludeElements, IBaseMetaType theMeta) {
607                boolean hasExtensions = false;
608                IBaseExtension<?, ?> sourceExtension = null;
609                if (theMeta instanceof IBaseHasExtensions) {
610                        List<? extends IBaseExtension<?, ?>> extensions = ((IBaseHasExtensions) theMeta).getExtension();
611                        if (!extensions.isEmpty()) {
612                                hasExtensions = true;
613
614                                /*
615                                 * FHIR DSTU3 did not have the Resource.meta.source field, so we use a
616                                 * custom HAPI FHIR extension in Resource.meta to store that field. However,
617                                 * we put the value for that field in a separate table, so we don't want to serialize
618                                 * it into the stored BLOB. Therefore: remove it from the resource temporarily
619                                 * and restore it afterward.
620                                 */
621                                if (myFhirContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) {
622                                        for (int i = 0; i < extensions.size(); i++) {
623                                                if (extensions.get(i).getUrl().equals(HapiExtensions.EXT_META_SOURCE)) {
624                                                        sourceExtension = extensions.remove(i);
625                                                        i--;
626                                                }
627                                        }
628                                }
629                                boolean allExtensionsRemoved = extensions.isEmpty();
630                                if (allExtensionsRemoved) {
631                                        hasExtensions = false;
632                                }
633                        }
634                }
635
636                theExcludeElements.add("id");
637                boolean inlineTagMode =
638                                getStorageSettings().getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.INLINE;
639                if (hasExtensions || inlineTagMode) {
640                        if (!inlineTagMode) {
641                                theExcludeElements.add(theResourceType + ".meta.profile");
642                                theExcludeElements.add(theResourceType + ".meta.tag");
643                                theExcludeElements.add(theResourceType + ".meta.security");
644                        }
645                        theExcludeElements.add(theResourceType + ".meta.versionId");
646                        theExcludeElements.add(theResourceType + ".meta.lastUpdated");
647                        theExcludeElements.add(theResourceType + ".meta.source");
648                } else {
649                        /*
650                         * If there are no extensions in the meta element, we can just exclude the
651                         * whole meta element, which avoids adding an empty "meta":{}
652                         * from showing up in the serialized JSON.
653                         */
654                        theExcludeElements.add(theResourceType + ".meta");
655                }
656                return sourceExtension;
657        }
658
659        protected boolean updateTags(
660                        TransactionDetails theTransactionDetails,
661                        RequestDetails theRequest,
662                        IBaseResource theResource,
663                        ResourceTable theEntity) {
664                Set<ResourceTag> allResourceTagsFromTheResource = new HashSet<>();
665                Set<ResourceTag> allOriginalResourceTagsFromTheEntity = getAllTagDefinitions(theEntity);
666
667                if (theResource instanceof IResource) {
668                        extractHapiTags(theTransactionDetails, (IResource) theResource, theEntity, allResourceTagsFromTheResource);
669                } else {
670                        extractRiTags(theTransactionDetails, (IAnyResource) theResource, theEntity, allResourceTagsFromTheResource);
671                }
672
673                extractProfileTags(theTransactionDetails, theResource, theEntity, allResourceTagsFromTheResource);
674
675                // the extract[Hapi|Ri|Profile]Tags methods above will have populated the allResourceTagsFromTheResource Set
676                // AND
677                // added all tags from theResource.meta.tags to theEntity.meta.tags.  the next steps are to:
678                // 1- remove duplicates;
679                // 2- remove tags from theEntity that are not present in theResource if header HEADER_META_SNAPSHOT_MODE
680                // is present in the request;
681                //
682                Set<ResourceTag> allResourceTagsNewAndOldFromTheEntity = getAllTagDefinitions(theEntity);
683                Set<TagDefinition> allTagDefinitionsPresent = new HashSet<>();
684
685                allResourceTagsNewAndOldFromTheEntity.forEach(tag -> {
686
687                        // Don't keep duplicate tags or tags with a missing definition
688                        TagDefinition tagDefinition = tag.getTag();
689                        if (tagDefinition == null || !allTagDefinitionsPresent.add(tagDefinition)) {
690                                theEntity.getTags().remove(tag);
691                        }
692
693                        // Drop any tags that have been removed
694                        if (tagDefinition != null && !allResourceTagsFromTheResource.contains(tag)) {
695                                if (shouldDroppedTagBeRemovedOnUpdate(theRequest, tag)) {
696                                        theEntity.getTags().remove(tag);
697                                } else if (HapiExtensions.EXT_SUBSCRIPTION_MATCHING_STRATEGY.equals(tagDefinition.getSystem())) {
698                                        theEntity.getTags().remove(tag);
699                                }
700                        }
701                });
702
703                // at this point, theEntity.meta.tags will be up to date:
704                // 1- it was stripped from tags that needed removing;
705                // 2- it has new tags from a resource update through theResource;
706                // 3- it has tags from the previous version;
707                //
708                // Since tags are merged on updates, we add tags from theEntity that theResource does not have
709                Set<ResourceTag> allUpdatedResourceTagsNewAndOldMinusRemovalsFromTheEntity = getAllTagDefinitions(theEntity);
710
711                allUpdatedResourceTagsNewAndOldMinusRemovalsFromTheEntity.forEach(aResourcetag -> {
712                        if (!allResourceTagsFromTheResource.contains(aResourcetag)) {
713                                IBaseCoding iBaseCoding = theResource
714                                                .getMeta()
715                                                .addTag()
716                                                .setCode(aResourcetag.getTag().getCode())
717                                                .setSystem(aResourcetag.getTag().getSystem())
718                                                .setVersion(aResourcetag.getTag().getVersion());
719
720                                allResourceTagsFromTheResource.add(aResourcetag);
721
722                                if (aResourcetag.getTag().getUserSelected() != null) {
723                                        iBaseCoding.setUserSelected(aResourcetag.getTag().getUserSelected());
724                                }
725                        }
726                });
727
728                theEntity.setHasTags(!allUpdatedResourceTagsNewAndOldMinusRemovalsFromTheEntity.isEmpty());
729                return !isEqualCollection(allOriginalResourceTagsFromTheEntity, allResourceTagsFromTheResource);
730        }
731
732        /**
733         * Subclasses may override to provide behaviour. Called when a pre-existing resource has been updated in the database
734         *
735         * @param theEntity The resource
736         */
737        protected void postDelete(ResourceTable theEntity) {
738                // nothing
739        }
740
741        /**
742         * Subclasses may override to provide behaviour. Called when a resource has been inserted into the database for the first time.
743         *
744         * @param theEntity         The entity being updated (Do not modify the entity! Undefined behaviour will occur!)
745         * @param theResource       The resource being persisted
746         * @param theRequestDetails The request details, needed for partition support
747         */
748        protected void postPersist(ResourceTable theEntity, T theResource, RequestDetails theRequestDetails) {
749                // nothing
750        }
751
752        /**
753         * Subclasses may override to provide behaviour. Called when a pre-existing resource has been updated in the database
754         *
755         * @param theEntity         The resource
756         * @param theResource       The resource being persisted
757         * @param theRequestDetails The request details, needed for partition support
758         */
759        protected void postUpdate(ResourceTable theEntity, T theResource, RequestDetails theRequestDetails) {
760                // nothing
761        }
762
763        @Override
764        @CoverageIgnore
765        public BaseHasResource readEntity(IIdType theValueId, RequestDetails theRequest) {
766                throw new NotImplementedException(Msg.code(927));
767        }
768
769        /**
770         * This method is called when an update to an existing resource detects that the resource supplied for update is missing a tag/profile/security label that the currently persisted resource holds.
771         * <p>
772         * The default implementation removes any profile declarations, but leaves tags and security labels in place. Subclasses may choose to override and change this behaviour.
773         * </p>
774         * <p>
775         * See <a href="http://hl7.org/fhir/resource.html#tag-updates">Updates to Tags, Profiles, and Security Labels</a> for a description of the logic that the default behaviour follows.
776         * </p>
777         *
778         * @param theTag The tag
779         * @return Returns <code>true</code> if the tag should be removed
780         */
781        protected boolean shouldDroppedTagBeRemovedOnUpdate(RequestDetails theRequest, ResourceTag theTag) {
782                if (theTag.getTag() == null) {
783                        return true;
784                }
785
786                Set<TagTypeEnum> metaSnapshotModeTokens = null;
787
788                if (theRequest != null) {
789                        List<String> metaSnapshotMode = theRequest.getHeaders(JpaConstants.HEADER_META_SNAPSHOT_MODE);
790                        if (metaSnapshotMode != null && !metaSnapshotMode.isEmpty()) {
791                                metaSnapshotModeTokens = new HashSet<>();
792                                for (String nextHeaderValue : metaSnapshotMode) {
793                                        StringTokenizer tok = new StringTokenizer(nextHeaderValue, ",");
794                                        while (tok.hasMoreTokens()) {
795                                                switch (trim(tok.nextToken())) {
796                                                        case "TAG":
797                                                                metaSnapshotModeTokens.add(TagTypeEnum.TAG);
798                                                                break;
799                                                        case "PROFILE":
800                                                                metaSnapshotModeTokens.add(TagTypeEnum.PROFILE);
801                                                                break;
802                                                        case "SECURITY_LABEL":
803                                                                metaSnapshotModeTokens.add(TagTypeEnum.SECURITY_LABEL);
804                                                                break;
805                                                }
806                                        }
807                                }
808                        }
809                }
810
811                if (metaSnapshotModeTokens == null) {
812                        metaSnapshotModeTokens = Collections.singleton(TagTypeEnum.PROFILE);
813                }
814
815                return metaSnapshotModeTokens.contains(theTag.getTag().getTagType());
816        }
817
818        String toResourceName(IBaseResource theResource) {
819                return myContext.getResourceType(theResource);
820        }
821
822        @VisibleForTesting
823        public void setEntityManager(EntityManager theEntityManager) {
824                myEntityManager = theEntityManager;
825        }
826
827        @VisibleForTesting
828        public void setSearchParamWithInlineReferencesExtractor(
829                        SearchParamWithInlineReferencesExtractor theSearchParamWithInlineReferencesExtractor) {
830                mySearchParamWithInlineReferencesExtractor = theSearchParamWithInlineReferencesExtractor;
831        }
832
833        @VisibleForTesting
834        public void setResourceHistoryTableDao(IResourceHistoryTableDao theResourceHistoryTableDao) {
835                myResourceHistoryTableDao = theResourceHistoryTableDao;
836        }
837
838        @VisibleForTesting
839        public void setDaoSearchParamSynchronizer(DaoSearchParamSynchronizer theDaoSearchParamSynchronizer) {
840                myDaoSearchParamSynchronizer = theDaoSearchParamSynchronizer;
841        }
842
843        private void verifyMatchUrlForConditionalCreateOrUpdate(
844                        CreateOrUpdateByMatch theCreateOrUpdate,
845                        IBaseResource theResource,
846                        String theIfNoneExist,
847                        ResourceIndexedSearchParams theParams,
848                        RequestDetails theRequestDetails) {
849                // Make sure that the match URL was actually appropriate for the supplied resource
850                InMemoryMatchResult outcome =
851                                myInMemoryResourceMatcher.match(theIfNoneExist, theResource, theParams, theRequestDetails);
852
853                if (outcome.supported() && !outcome.matched()) {
854                        String errorMsg = getConditionalCreateOrUpdateErrorMsg(theCreateOrUpdate);
855                        throw new InvalidRequestException(Msg.code(929) + errorMsg);
856                }
857        }
858
859        private String getConditionalCreateOrUpdateErrorMsg(CreateOrUpdateByMatch theCreateOrUpdate) {
860                return String.format(
861                                "Failed to process conditional %s. " + "The supplied resource did not satisfy the conditional URL.",
862                                theCreateOrUpdate.name().toLowerCase());
863        }
864
865        @SuppressWarnings("unchecked")
866        @Override
867        public ResourceTable updateEntity(
868                        RequestDetails theRequest,
869                        final IBaseResource theResource,
870                        IBasePersistedResource theEntity,
871                        Date theDeletedTimestampOrNull,
872                        boolean thePerformIndexing,
873                        boolean theUpdateVersion,
874                        TransactionDetails theTransactionDetails,
875                        boolean theForceUpdate,
876                        boolean theCreateNewHistoryEntry) {
877                Validate.notNull(theEntity, "entity must not be null");
878                Validate.isTrue(
879                                theDeletedTimestampOrNull != null || theResource != null,
880                                "Must have either a resource[%s] or a deleted timestamp[%s] for resource PID[%s]",
881                                theDeletedTimestampOrNull != null,
882                                theResource != null,
883                                theEntity.getPersistentId());
884
885                ourLog.debug("Starting entity update");
886
887                ResourceTable entity = (ResourceTable) theEntity;
888
889                /*
890                 * This should be the very first thing..
891                 */
892                if (theResource != null) {
893                        if (thePerformIndexing && theDeletedTimestampOrNull == null) {
894                                if (!ourValidationDisabledForUnitTest) {
895                                        validateResourceForStorage((T) theResource, entity);
896                                }
897                        }
898                        if (!StringUtils.isBlank(entity.getResourceType())) {
899                                String resourceType = myContext.getResourceType(theResource);
900                                // This is just a sanity check and should never actually fail.
901                                // We resolve the ID using IdLookupService, and there should be
902                                // no way to get it to give you a mismatched type for an ID.
903                                Validate.isTrue(resourceType.equals(entity.getResourceType()));
904                        }
905                }
906
907                if (entity.getPublished() == null) {
908                        ourLog.debug("Entity has published time: {}", theTransactionDetails.getTransactionDate());
909                        entity.setPublished(theTransactionDetails.getTransactionDate());
910                }
911
912                ResourceIndexedSearchParams existingParams = null;
913
914                ResourceIndexedSearchParams newParams = null;
915
916                EncodedResource changed;
917                if (theDeletedTimestampOrNull != null) {
918                        // DELETE
919
920                        entity.setDeleted(theDeletedTimestampOrNull);
921                        entity.setUpdated(theDeletedTimestampOrNull);
922                        changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
923
924                } else {
925
926                        // CREATE or UPDATE
927
928                        IdentityHashMap<ResourceTable, ResourceIndexedSearchParams> existingSearchParams =
929                                        getSearchParamsMapFromTransaction(theTransactionDetails);
930                        existingParams = existingSearchParams.get(entity);
931                        if (existingParams == null) {
932                                existingParams = ResourceIndexedSearchParams.withLists(entity);
933                                /*
934                                 * If we have lots of resource links, this proactively fetches the targets so
935                                 * that we don't look them up one-by-one when comparing the new set to the
936                                 * old set later on
937                                 */
938                                if (existingParams.getResourceLinks().size() >= 10) {
939                                        List<Long> allPids = existingParams.getResourceLinks().stream()
940                                                        .map(ResourceLink::getId)
941                                                        .collect(Collectors.toList());
942                                        new QueryChunker<Long>().chunk(allPids, chunkPids -> {
943                                                List<ResourceLink> targets = myResourceLinkDao.findByPidAndFetchTargetDetails(chunkPids);
944                                                ourLog.trace("Prefetched targets: {}", targets);
945                                        });
946                                }
947                                existingSearchParams.put(entity, existingParams);
948                        }
949                        entity.setDeleted(null);
950
951                        // TODO: is this IF statement always true? Try removing it
952                        if (thePerformIndexing || theEntity.getVersion() == 1) {
953
954                                newParams = ResourceIndexedSearchParams.withSets();
955
956                                RequestPartitionId requestPartitionId;
957                                if (!myPartitionSettings.isPartitioningEnabled()) {
958                                        requestPartitionId = RequestPartitionId.allPartitions();
959                                } else if (entity.getPartitionId() != null) {
960                                        requestPartitionId = entity.getPartitionId().toPartitionId();
961                                } else {
962                                        requestPartitionId = myPartitionSettings.getDefaultRequestPartitionId();
963                                }
964
965                                // Extract search params for resource
966                                mySearchParamWithInlineReferencesExtractor.populateFromResource(
967                                                requestPartitionId,
968                                                newParams,
969                                                theTransactionDetails,
970                                                entity,
971                                                theResource,
972                                                existingParams,
973                                                theRequest,
974                                                thePerformIndexing);
975
976                                if (CollectionUtils.isNotEmpty(newParams.myLinks)) {
977                                        setTargetResourceTypeIdForResourceLinks(newParams.myLinks);
978                                }
979
980                                // Actually persist the ResourceTable and ResourceHistoryTable entities
981                                changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
982
983                                if (theForceUpdate) {
984                                        changed.setChanged(true);
985                                }
986
987                                if (changed.isChanged()) {
988                                        checkConditionalMatch(
989                                                        entity, theUpdateVersion, theResource, thePerformIndexing, newParams, theRequest);
990
991                                        if (CURRENTLY_REINDEXING.get(theResource) != Boolean.TRUE) {
992                                                entity.setUpdated(theTransactionDetails.getTransactionDate());
993                                        }
994                                        newParams.populateResourceTableSearchParamsPresentFlags(entity);
995                                }
996
997                        } else {
998
999                                entity.setUpdated(theTransactionDetails.getTransactionDate());
1000                                changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, false);
1001                        }
1002                }
1003
1004                if (changed != null && changed.isChanged()) {
1005                        populateFullTextFieldsAndSetEntityStatus(theRequest, myContext, theResource, entity, newParams);
1006                }
1007
1008                if (thePerformIndexing
1009                                && changed != null
1010                                && !changed.isChanged()
1011                                && !theForceUpdate
1012                                && myStorageSettings.isSuppressUpdatesWithNoChange()
1013                                && (entity.getVersion() > 1 || theUpdateVersion)) {
1014                        ourLog.debug(
1015                                        "Resource {} has not changed",
1016                                        entity.getIdDt().toUnqualified().getValue());
1017                        if (theResource != null) {
1018                                myJpaStorageResourceParser.updateResourceMetadata(entity, theResource);
1019                        }
1020                        entity.setUnchangedInCurrentOperation(true);
1021                        return entity;
1022                }
1023
1024                if (entity.getId().getId() != null && theUpdateVersion) {
1025                        entity.markVersionUpdatedInCurrentTransaction();
1026                }
1027
1028                /*
1029                 * Save the resource itself
1030                 */
1031                if (entity.getId().getId() == null) {
1032                        myEntityManager.persist(entity);
1033
1034                        if (entity.getFhirId() == null) {
1035                                entity.setFhirId(Long.toString(entity.getId().getId()));
1036                        }
1037
1038                        postPersist(entity, (T) theResource, theRequest);
1039
1040                } else if (entity.getDeleted() != null) {
1041                        entity = myEntityManager.merge(entity);
1042
1043                        postDelete(entity);
1044
1045                } else {
1046                        entity = myEntityManager.merge(entity);
1047
1048                        postUpdate(entity, (T) theResource, theRequest);
1049                }
1050
1051                if (theCreateNewHistoryEntry) {
1052                        createHistoryEntry(theRequest, theResource, entity, changed);
1053                }
1054
1055                /*
1056                 * Update the "search param present" table which is used for the
1057                 * ?foo:missing=true queries
1058                 *
1059                 * Note that we're only populating this for reference params
1060                 * because the index tables for all other types have a MISSING column
1061                 * right on them for handling the :missing queries. We can't use the
1062                 * index table for resource links (reference indexes) because we index
1063                 * those by path and not by parameter name.
1064                 */
1065                if (thePerformIndexing && newParams != null) {
1066                        AddRemoveCount presenceCount =
1067                                        mySearchParamPresenceSvc.updatePresence(entity, newParams.mySearchParamPresentEntities);
1068
1069                        // Interceptor broadcast: JPA_PERFTRACE_INFO
1070                        if (!presenceCount.isEmpty()) {
1071                                IInterceptorBroadcaster compositeBroadcaster =
1072                                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest);
1073                                if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) {
1074                                        StorageProcessingMessage message = new StorageProcessingMessage();
1075                                        message.setMessage(
1076                                                        "For " + entity.getIdDt().toUnqualifiedVersionless().getValue() + " added "
1077                                                                        + presenceCount.getAddCount() + " and removed " + presenceCount.getRemoveCount()
1078                                                                        + " resource search parameter presence entries");
1079                                        HookParams params = new HookParams()
1080                                                        .add(RequestDetails.class, theRequest)
1081                                                        .addIfMatchesType(ServletRequestDetails.class, theRequest)
1082                                                        .add(StorageProcessingMessage.class, message);
1083                                        compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params);
1084                                }
1085                        }
1086                }
1087
1088                /*
1089                 * Indexing
1090                 */
1091                if (thePerformIndexing) {
1092                        if (newParams == null) {
1093                                myExpungeService.deleteAllSearchParams(entity.getPersistentId());
1094                                entity.clearAllParamsPopulated();
1095                        } else {
1096
1097                                // Synchronize search param indexes
1098                                AddRemoveCount searchParamAddRemoveCount =
1099                                                myDaoSearchParamSynchronizer.synchronizeSearchParamsToDatabase(
1100                                                                newParams, entity, existingParams);
1101
1102                                newParams.populateResourceTableParamCollections(entity);
1103
1104                                // Interceptor broadcast: JPA_PERFTRACE_INFO
1105                                if (!searchParamAddRemoveCount.isEmpty()) {
1106                                        IInterceptorBroadcaster compositeBroadcaster =
1107                                                        CompositeInterceptorBroadcaster.newCompositeBroadcaster(
1108                                                                        myInterceptorBroadcaster, theRequest);
1109                                        if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) {
1110                                                StorageProcessingMessage message = new StorageProcessingMessage();
1111                                                message.setMessage("For "
1112                                                                + entity.getIdDt().toUnqualifiedVersionless().getValue() + " added "
1113                                                                + searchParamAddRemoveCount.getAddCount() + " and removed "
1114                                                                + searchParamAddRemoveCount.getRemoveCount()
1115                                                                + " resource search parameter index entries");
1116                                                HookParams params = new HookParams()
1117                                                                .add(RequestDetails.class, theRequest)
1118                                                                .addIfMatchesType(ServletRequestDetails.class, theRequest)
1119                                                                .add(StorageProcessingMessage.class, message);
1120                                                compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params);
1121                                        }
1122                                }
1123
1124                                // Put the final set of search params into the transaction
1125                                getSearchParamsMapFromTransaction(theTransactionDetails).put(entity, newParams);
1126                        }
1127                }
1128
1129                if (theResource != null) {
1130                        myJpaStorageResourceParser.updateResourceMetadata(entity, theResource);
1131                }
1132
1133                return entity;
1134        }
1135
1136        private static IdentityHashMap<ResourceTable, ResourceIndexedSearchParams> getSearchParamsMapFromTransaction(
1137                        TransactionDetails theTransactionDetails) {
1138                return theTransactionDetails.getOrCreateUserData(
1139                                HapiTransactionService.XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS, IdentityHashMap::new);
1140        }
1141
1142        /**
1143         * Make sure that the match URL was actually appropriate for the supplied
1144         * resource, if so configured, or do it only for first version, since technically it
1145         * is possible (and legal) for someone to be using a conditional update
1146         * to match a resource and then update it in a way that it no longer
1147         * matches.
1148         */
1149        private void checkConditionalMatch(
1150                        ResourceTable theEntity,
1151                        boolean theUpdateVersion,
1152                        IBaseResource theResource,
1153                        boolean thePerformIndexing,
1154                        ResourceIndexedSearchParams theNewParams,
1155                        RequestDetails theRequest) {
1156
1157                if (!thePerformIndexing) {
1158                        return;
1159                }
1160
1161                if (theEntity.getCreatedByMatchUrl() == null && theEntity.getUpdatedByMatchUrl() == null) {
1162                        return;
1163                }
1164
1165                // version is not updated at this point, but could be pending for update, which we consider here
1166                long pendingVersion = theEntity.getVersion();
1167                if (theUpdateVersion && !theEntity.isVersionUpdatedInCurrentTransaction()) {
1168                        pendingVersion++;
1169                }
1170
1171                if (myStorageSettings.isPreventInvalidatingConditionalMatchCriteria() || pendingVersion <= 1L) {
1172                        String createOrUpdateUrl;
1173                        CreateOrUpdateByMatch createOrUpdate;
1174
1175                        if (theEntity.getCreatedByMatchUrl() != null) {
1176                                createOrUpdateUrl = theEntity.getCreatedByMatchUrl();
1177                                createOrUpdate = CreateOrUpdateByMatch.CREATE;
1178                        } else {
1179                                createOrUpdateUrl = theEntity.getUpdatedByMatchUrl();
1180                                createOrUpdate = CreateOrUpdateByMatch.UPDATE;
1181                        }
1182
1183                        verifyMatchUrlForConditionalCreateOrUpdate(
1184                                        createOrUpdate, theResource, createOrUpdateUrl, theNewParams, theRequest);
1185                }
1186        }
1187
1188        public IBasePersistedResource<?> updateHistoryEntity(
1189                        RequestDetails theRequest,
1190                        T theResource,
1191                        IBasePersistedResource<?> theEntity,
1192                        IBasePersistedResource<?> theHistoryEntity,
1193                        IIdType theResourceId,
1194                        TransactionDetails theTransactionDetails,
1195                        boolean isUpdatingCurrent) {
1196                Validate.notNull(theEntity);
1197                Validate.isTrue(
1198                                theResource != null,
1199                                "Must have either a resource[%s] for resource PID[%s]",
1200                                theResource != null,
1201                                theEntity.getPersistentId());
1202
1203                ourLog.debug("Starting history entity update");
1204                EncodedResource encodedResource = new EncodedResource();
1205                ResourceHistoryTable historyEntity;
1206
1207                if (isUpdatingCurrent) {
1208                        ResourceTable entity = (ResourceTable) theEntity;
1209
1210                        IBaseResource oldResource;
1211                        if (getStorageSettings().isMassIngestionMode()) {
1212                                oldResource = null;
1213                        } else {
1214                                oldResource = myJpaStorageResourceParser.toResource(entity, false);
1215                        }
1216
1217                        notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, true);
1218
1219                        ResourceTable savedEntity = updateEntity(
1220                                        theRequest, theResource, entity, null, true, false, theTransactionDetails, false, false);
1221                        // Have to call populate again for the encodedResource, since using createHistoryEntry() will cause version
1222                        // constraint failure, ie updating the same resource at the same time
1223                        encodedResource = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
1224                        // For some reason the current version entity is not attached until after using updateEntity
1225                        historyEntity = ((ResourceTable) readEntity(theResourceId, theRequest)).getCurrentVersionEntity();
1226
1227                        // Update version/lastUpdated so that interceptors see the correct version
1228                        myJpaStorageResourceParser.updateResourceMetadata(savedEntity, theResource);
1229                        // Populate the PID in the resource, so it is available to hooks
1230                        addPidToResource(savedEntity, theResource);
1231
1232                        if (!savedEntity.isUnchangedInCurrentOperation()) {
1233                                notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, false);
1234                        }
1235                } else {
1236                        historyEntity = (ResourceHistoryTable) theHistoryEntity;
1237                        if (!StringUtils.isBlank(historyEntity.getResourceType())) {
1238                                String resourceType = myContext.getResourceType(theResource);
1239                                if (!resourceType.equals(historyEntity.getResourceType())) {
1240                                        throw new UnprocessableEntityException(Msg.code(930) + "Existing resource ID["
1241                                                        + historyEntity.getIdDt().toUnqualifiedVersionless() + "] is of type["
1242                                                        + historyEntity.getResourceType()
1243                                                        + "] - Cannot update with [" + resourceType + "]");
1244                                }
1245                        }
1246
1247                        historyEntity.setDeleted(null);
1248
1249                        // Check if resource is the same
1250                        ResourceEncodingEnum encoding = myStorageSettings.getResourceEncoding();
1251                        List<String> excludeElements = new ArrayList<>(8);
1252                        getExcludedElements(historyEntity.getResourceType(), excludeElements, theResource.getMeta());
1253                        String encodedResourceString =
1254                                        myResourceHistoryCalculator.encodeResource(theResource, encoding, excludeElements);
1255                        byte[] resourceBinary = ResourceHistoryCalculator.getResourceBinary(encoding, encodedResourceString);
1256                        final boolean changed = myResourceHistoryCalculator.isResourceHistoryChanged(
1257                                        historyEntity, resourceBinary, encodedResourceString);
1258
1259                        historyEntity.setUpdated(theTransactionDetails.getTransactionDate());
1260
1261                        if (!changed && myStorageSettings.isSuppressUpdatesWithNoChange() && (historyEntity.getVersion() > 1)) {
1262                                ourLog.debug(
1263                                                "Resource {} has not changed",
1264                                                historyEntity.getIdDt().toUnqualified().getValue());
1265                                myJpaStorageResourceParser.updateResourceMetadata(historyEntity, theResource);
1266                                return historyEntity;
1267                        }
1268
1269                        myResourceHistoryCalculator.populateEncodedResource(
1270                                        encodedResource, encodedResourceString, resourceBinary, encoding);
1271                }
1272                /*
1273                 * Save the resource itself to the resourceHistoryTable
1274                 */
1275                historyEntity = myEntityManager.merge(historyEntity);
1276                historyEntity.setEncoding(encodedResource.getEncoding());
1277                historyEntity.setResource(encodedResource.getResourceBinary());
1278                historyEntity.setResourceTextVc(encodedResource.getResourceText());
1279                myResourceHistoryTableDao.save(historyEntity);
1280
1281                myJpaStorageResourceParser.updateResourceMetadata(historyEntity, theResource);
1282
1283                return historyEntity;
1284        }
1285
1286        private void populateEncodedResource(
1287                        EncodedResource encodedResource,
1288                        String encodedResourceString,
1289                        byte[] theResourceBinary,
1290                        ResourceEncodingEnum theEncoding) {
1291                encodedResource.setResourceText(encodedResourceString);
1292                encodedResource.setResourceBinary(theResourceBinary);
1293                encodedResource.setEncoding(theEncoding);
1294        }
1295
1296        private void createHistoryEntry(
1297                        RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity, EncodedResource theChanged) {
1298                boolean versionedTags =
1299                                getStorageSettings().getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.VERSIONED;
1300
1301                ResourceHistoryTable historyEntry = null;
1302                long resourceVersion = theEntity.getVersion();
1303                if (!myStorageSettings.isResourceDbHistoryEnabled() && resourceVersion > 1L) {
1304                        /*
1305                         * If we're not storing history, then just pull the current history
1306                         * table row and update it. Note that there is always a chance that
1307                         * this could return null if the current resourceVersion has been expunged
1308                         * in which case we'll still create a new one
1309                         */
1310                        historyEntry = myResourceHistoryTableDao.findForIdAndVersion(
1311                                        theEntity.getResourceId().toFk(), resourceVersion - 1);
1312                        if (historyEntry != null) {
1313                                theEntity.populateHistoryEntityVersionAndDates(historyEntry);
1314                                if (versionedTags && theEntity.isHasTags()) {
1315                                        for (ResourceTag next : theEntity.getTags()) {
1316                                                historyEntry.addTag(next.getTag());
1317                                        }
1318                                }
1319                        }
1320                }
1321
1322                /*
1323                 * This should basically always be null unless resource history
1324                 * is disabled on this server. In that case, we'll just be reusing
1325                 * the previous version entity.
1326                 */
1327                if (historyEntry == null) {
1328                        historyEntry = theEntity.toHistory(versionedTags && theEntity.getDeleted() == null);
1329                }
1330
1331                historyEntry.setEncoding(theChanged.getEncoding());
1332                historyEntry.setResource(theChanged.getResourceBinary());
1333                historyEntry.setResourceTextVc(theChanged.getResourceText());
1334
1335                ourLog.debug("Saving history entry ID[{}] for RES_ID[{}]", historyEntry.getId(), historyEntry.getResourceId());
1336                myEntityManager.persist(historyEntry);
1337                theEntity.setCurrentVersionEntity(historyEntry);
1338
1339                // Save resource source
1340                String source = null;
1341
1342                if (theResource != null) {
1343                        if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) {
1344                                IBaseMetaType meta = theResource.getMeta();
1345                                source = MetaUtil.getSource(myContext, meta);
1346                        }
1347                        if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) {
1348                                source = ((IBaseHasExtensions) theResource.getMeta())
1349                                                .getExtension().stream()
1350                                                                .filter(t -> HapiExtensions.EXT_META_SOURCE.equals(t.getUrl()))
1351                                                                .filter(t -> t.getValue() instanceof IPrimitiveType)
1352                                                                .map(t -> ((IPrimitiveType<?>) t.getValue()).getValueAsString())
1353                                                                .findFirst()
1354                                                                .orElse(null);
1355                        }
1356                }
1357
1358                String requestId = getRequestId(theRequest, source);
1359                source = MetaUtil.cleanProvenanceSourceUriOrEmpty(source);
1360
1361                boolean shouldStoreSource =
1362                                myStorageSettings.getStoreMetaSourceInformation().isStoreSourceUri();
1363                boolean shouldStoreRequestId =
1364                                myStorageSettings.getStoreMetaSourceInformation().isStoreRequestId();
1365                boolean haveSource = isNotBlank(source) && shouldStoreSource;
1366                boolean haveRequestId = isNotBlank(requestId) && shouldStoreRequestId;
1367                if (haveSource || haveRequestId) {
1368                        if (haveRequestId) {
1369                                String persistedRequestId = left(requestId, Constants.REQUEST_ID_LENGTH);
1370                                historyEntry.setRequestId(persistedRequestId);
1371                        }
1372                        if (haveSource) {
1373                                String persistedSource = left(source, ResourceHistoryTable.SOURCE_URI_LENGTH);
1374                                historyEntry.setSourceUri(persistedSource);
1375                        }
1376                        if (theResource != null) {
1377                                MetaUtil.populateResourceSource(
1378                                                myFhirContext,
1379                                                shouldStoreSource ? source : null,
1380                                                shouldStoreRequestId ? requestId : null,
1381                                                theResource);
1382                        }
1383                }
1384        }
1385
1386        private String getRequestId(RequestDetails theRequest, String theSource) {
1387                if (myStorageSettings.isPreserveRequestIdInResourceBody()) {
1388                        return StringUtils.substringAfter(theSource, "#");
1389                }
1390                return theRequest != null ? theRequest.getRequestId() : null;
1391        }
1392
1393        @Override
1394        public DaoMethodOutcome updateInternal(
1395                        RequestDetails theRequestDetails,
1396                        T theResource,
1397                        String theMatchUrl,
1398                        boolean thePerformIndexing,
1399                        boolean theForceUpdateVersion,
1400                        IBasePersistedResource theEntity,
1401                        IIdType theResourceId,
1402                        @Nullable IBaseResource theOldResource,
1403                        RestOperationTypeEnum theOperationType,
1404                        TransactionDetails theTransactionDetails) {
1405
1406                ResourceTable entity = (ResourceTable) theEntity;
1407
1408                // We'll update the resource ID with the correct version later but for
1409                // now at least set it to something useful for the interceptors
1410                theResource.setId(entity.getIdDt());
1411
1412                // Notify IServerOperationInterceptors about pre-action call
1413                notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, true);
1414
1415                entity.setUpdatedByMatchUrl(theMatchUrl);
1416
1417                // Perform update
1418                ResourceTable savedEntity = updateEntity(
1419                                theRequestDetails,
1420                                theResource,
1421                                entity,
1422                                null,
1423                                thePerformIndexing,
1424                                thePerformIndexing,
1425                                theTransactionDetails,
1426                                theForceUpdateVersion,
1427                                thePerformIndexing);
1428
1429                /*
1430                 * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
1431                 * we'll manually increase the version. This is important because we want the updated version number
1432                 * to be reflected in the resource shared with interceptors
1433                 */
1434                if (!thePerformIndexing
1435                                && !savedEntity.isUnchangedInCurrentOperation()
1436                                && !ourDisableIncrementOnUpdateForUnitTest) {
1437                        if (!theResourceId.hasVersionIdPart()) {
1438                                theResourceId = theResourceId.withVersion(Long.toString(savedEntity.getVersion()));
1439                        }
1440                        incrementId(theResource, savedEntity, theResourceId);
1441                }
1442
1443                // Update version/lastUpdated so that interceptors see the correct version
1444                myJpaStorageResourceParser.updateResourceMetadata(savedEntity, theResource);
1445
1446                // Populate the PID in the resource so it is available to hooks
1447                addPidToResource(savedEntity, theResource);
1448
1449                // Notify interceptors
1450                if (!savedEntity.isUnchangedInCurrentOperation()) {
1451                        notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, false);
1452                }
1453
1454                Collection<? extends BaseTag> tagList = Collections.emptyList();
1455                if (entity.isHasTags()) {
1456                        tagList = entity.getTags();
1457                }
1458                long version = entity.getVersion();
1459                myJpaStorageResourceParser.populateResourceMetadata(entity, false, tagList, version, theResource);
1460
1461                boolean wasDeleted = false;
1462                if (theOldResource != null) {
1463                        wasDeleted = theOldResource.isDeleted();
1464                }
1465
1466                if (wasDeleted && !myStorageSettings.isDeleteEnabled()) {
1467                        String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "cantUndeleteWithDeletesDisabled");
1468                        throw new InvalidRequestException(Msg.code(2573) + msg);
1469                }
1470
1471                DaoMethodOutcome outcome = toMethodOutcome(
1472                                                theRequestDetails, savedEntity, theResource, theMatchUrl, theOperationType)
1473                                .setCreated(wasDeleted);
1474
1475                if (!thePerformIndexing) {
1476                        IIdType id = getContext().getVersion().newIdType();
1477                        id.setValue(entity.getIdDt().getValue());
1478                        outcome.setId(id);
1479                }
1480
1481                // Only include a task timer if we're not in a sub-request (i.e. a transaction)
1482                // since individual item times don't actually make much sense in the context
1483                // of a transaction
1484                StopWatch w = null;
1485                if (theRequestDetails != null && !theRequestDetails.isSubRequest()) {
1486                        if (theTransactionDetails != null && !theTransactionDetails.isFhirTransaction()) {
1487                                w = new StopWatch(theTransactionDetails.getTransactionDate());
1488                        }
1489                }
1490
1491                populateOperationOutcomeForUpdate(w, outcome, theMatchUrl, outcome.getOperationType(), theTransactionDetails);
1492
1493                return outcome;
1494        }
1495
1496        private void notifyInterceptors(
1497                        RequestDetails theRequestDetails,
1498                        T theResource,
1499                        IBaseResource theOldResource,
1500                        TransactionDetails theTransactionDetails,
1501                        boolean isUnchanged) {
1502                Pointcut interceptorPointcut = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED;
1503
1504                HookParams hookParams = new HookParams()
1505                                .add(IBaseResource.class, theOldResource)
1506                                .add(IBaseResource.class, theResource)
1507                                .add(RequestDetails.class, theRequestDetails)
1508                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
1509                                .add(TransactionDetails.class, theTransactionDetails);
1510
1511                if (!isUnchanged) {
1512                        hookParams.add(
1513                                        InterceptorInvocationTimingEnum.class,
1514                                        theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
1515                        interceptorPointcut = Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED;
1516                }
1517
1518                doCallHooks(theTransactionDetails, theRequestDetails, interceptorPointcut, hookParams);
1519        }
1520
1521        protected void addPidToResource(IResourceLookup<JpaPid> theEntity, IBaseResource theResource) {
1522                if (theResource instanceof IAnyResource) {
1523                        IDao.RESOURCE_PID.put(theResource, theEntity.getPersistentId());
1524                } else if (theResource instanceof IResource) {
1525                        IDao.RESOURCE_PID.put(theResource, theEntity.getPersistentId());
1526                }
1527        }
1528
1529        private void validateChildReferenceTargetTypes(IBase theElement, String thePath) {
1530                if (theElement == null) {
1531                        return;
1532                }
1533                BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass());
1534                if (!(def instanceof BaseRuntimeElementCompositeDefinition)) {
1535                        return;
1536                }
1537
1538                BaseRuntimeElementCompositeDefinition<?> cdef = (BaseRuntimeElementCompositeDefinition<?>) def;
1539                for (BaseRuntimeChildDefinition nextChildDef : cdef.getChildren()) {
1540
1541                        List<IBase> values = nextChildDef.getAccessor().getValues(theElement);
1542                        if (values == null || values.isEmpty()) {
1543                                continue;
1544                        }
1545
1546                        String newPath = thePath + "." + nextChildDef.getElementName();
1547
1548                        for (IBase nextChild : values) {
1549                                validateChildReferenceTargetTypes(nextChild, newPath);
1550                        }
1551
1552                        if (nextChildDef instanceof RuntimeChildResourceDefinition) {
1553                                RuntimeChildResourceDefinition nextChildDefRes = (RuntimeChildResourceDefinition) nextChildDef;
1554                                Set<String> validTypes = new HashSet<>();
1555                                boolean allowAny = false;
1556                                for (Class<? extends IBaseResource> nextValidType : nextChildDefRes.getResourceTypes()) {
1557                                        if (nextValidType.isInterface()) {
1558                                                allowAny = true;
1559                                                break;
1560                                        }
1561                                        validTypes.add(getContext().getResourceType(nextValidType));
1562                                }
1563
1564                                if (allowAny) {
1565                                        continue;
1566                                }
1567
1568                                if (getStorageSettings().isEnforceReferenceTargetTypes()) {
1569                                        for (IBase nextChild : values) {
1570                                                IBaseReference nextRef = (IBaseReference) nextChild;
1571                                                IIdType referencedId = nextRef.getReferenceElement();
1572                                                if (!isBlank(referencedId.getResourceType())) {
1573                                                        if (!isLogicalReference(referencedId)) {
1574                                                                if (!referencedId.getValue().contains("?")) {
1575                                                                        if (!validTypes.contains(referencedId.getResourceType())) {
1576                                                                                throw new UnprocessableEntityException(Msg.code(931)
1577                                                                                                + "Invalid reference found at path '" + newPath + "'. Resource type '"
1578                                                                                                + referencedId.getResourceType() + "' is not valid for this path");
1579                                                                        }
1580                                                                }
1581                                                        }
1582                                                }
1583                                        }
1584                                }
1585                        }
1586                }
1587        }
1588
1589        protected void validateMetaCount(int theMetaCount) {
1590                if (myStorageSettings.getResourceMetaCountHardLimit() != null) {
1591                        if (theMetaCount > myStorageSettings.getResourceMetaCountHardLimit()) {
1592                                throw new UnprocessableEntityException(Msg.code(932) + "Resource contains " + theMetaCount
1593                                                + " meta entries (tag/profile/security label), maximum is "
1594                                                + myStorageSettings.getResourceMetaCountHardLimit());
1595                        }
1596                }
1597        }
1598
1599        /**
1600         * This method is invoked immediately before storing a new resource, or an update to an existing resource to allow the DAO to ensure that it is valid for persistence. By default, checks for the
1601         * "subsetted" tag and rejects resources which have it. Subclasses should call the superclass implementation to preserve this check.
1602         *
1603         * @param theResource     The resource that is about to be persisted
1604         * @param theEntityToSave TODO
1605         */
1606        protected void validateResourceForStorage(T theResource, ResourceTable theEntityToSave) {
1607                Object tag = null;
1608
1609                int totalMetaCount = 0;
1610
1611                if (theResource instanceof IResource) {
1612                        IResource res = (IResource) theResource;
1613                        TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(res);
1614                        if (tagList != null) {
1615                                tag = tagList.getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE);
1616                                totalMetaCount += tagList.size();
1617                        }
1618                        List<IdDt> profileList = ResourceMetadataKeyEnum.PROFILES.get(res);
1619                        if (profileList != null) {
1620                                totalMetaCount += profileList.size();
1621                        }
1622                } else {
1623                        IAnyResource res = (IAnyResource) theResource;
1624                        tag = res.getMeta().getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE);
1625                        totalMetaCount += res.getMeta().getTag().size();
1626                        totalMetaCount += res.getMeta().getProfile().size();
1627                        totalMetaCount += res.getMeta().getSecurity().size();
1628                }
1629
1630                if (tag != null) {
1631                        throw new UnprocessableEntityException(
1632                                        Msg.code(933)
1633                                                        + "Resource contains the 'subsetted' tag, and must not be stored as it may contain a subset of available data");
1634                }
1635
1636                if (getStorageSettings().isEnforceReferenceTargetTypes()) {
1637                        String resName = getContext().getResourceType(theResource);
1638                        validateChildReferenceTargetTypes(theResource, resName);
1639                }
1640
1641                validateMetaCount(totalMetaCount);
1642        }
1643
1644        @PostConstruct
1645        public void start() {}
1646
1647        @VisibleForTesting
1648        public void setStorageSettingsForUnitTest(JpaStorageSettings theStorageSettings) {
1649                myStorageSettings = theStorageSettings;
1650        }
1651
1652        /**
1653         * If configured to do so, extracts the FullText indexes for the given
1654         * entity. The {@link ResourceTable#setIndexStatus(EntityIndexStatusEnum) Index Status}
1655         * is updated to reflect whether fulltext indexing is being used on this entity.
1656         */
1657        private void populateFullTextFieldsAndSetEntityStatus(
1658                        RequestDetails theRequestDetails,
1659                        final FhirContext theContext,
1660                        final IBaseResource theResource,
1661                        ResourceTable theEntity,
1662                        ResourceIndexedSearchParams theNewParams) {
1663                if (myFulltextSearchSvc == null || myFulltextSearchSvc.isDisabled()) {
1664                        theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_RDBMS_ONLY);
1665                        return;
1666                }
1667
1668                // This will get changed if we end up setting either
1669                theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_RDBMS_ONLY);
1670
1671                // Standard FullText indexing
1672                if (myStorageSettings.isHibernateSearchIndexFullText()) {
1673
1674                        // _content
1675                        if (mySearchParamRegistry.hasActiveSearchParam(
1676                                        theEntity.getResourceType(),
1677                                        Constants.PARAM_CONTENT,
1678                                        ISearchParamRegistry.SearchParamLookupContextEnum.INDEX)) {
1679                                Supplier<String> contentSupplier = () -> parseContentTextIntoWords(theContext, theResource);
1680                                Pointcut pointcut = Pointcut.JPA_INDEX_EXTRACT_FULLTEXT_CONTENT;
1681                                Consumer<String> contentEntitySetter = theEntity::setContentText;
1682                                extractFullTextIndexData(
1683                                                theRequestDetails, theResource, theEntity, pointcut, contentSupplier, contentEntitySetter);
1684                        }
1685
1686                        // _text
1687                        if (mySearchParamRegistry.hasActiveSearchParam(
1688                                        theEntity.getResourceType(),
1689                                        Constants.PARAM_TEXT,
1690                                        ISearchParamRegistry.SearchParamLookupContextEnum.INDEX)) {
1691                                Supplier<String> textSupplier = () -> parseNarrativeTextIntoWords(theResource);
1692                                Pointcut pointcut = Pointcut.JPA_INDEX_EXTRACT_FULLTEXT_TEXT;
1693                                Consumer<String> textEntitySetter = theEntity::setNarrativeText;
1694                                extractFullTextIndexData(
1695                                                theRequestDetails, theResource, theEntity, pointcut, textSupplier, textEntitySetter);
1696                        }
1697                }
1698
1699                // Advanced indexing - Index standard search params in the FullText index
1700                if (myStorageSettings.isHibernateSearchIndexSearchParams()) {
1701                        if (theResource != null) {
1702                                ExtendedHSearchIndexData hSearchIndexData =
1703                                                myFulltextSearchSvc.extractLuceneIndexData(theResource, theEntity, theNewParams);
1704                                theEntity.setLuceneIndexData(hSearchIndexData);
1705                        } else {
1706                                theEntity.setLuceneIndexData(null);
1707                        }
1708                        theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_ALL);
1709                }
1710        }
1711
1712        private void extractFullTextIndexData(
1713                        RequestDetails theRequestDetails,
1714                        IBaseResource theResource,
1715                        ResourceTable theEntity,
1716                        Pointcut thePointcut,
1717                        Supplier<String> theContentSupplier,
1718                        Consumer<String> theEntityIndexSetter) {
1719                IInterceptorBroadcaster compositeBroadcaster =
1720                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails);
1721                FullTextExtractionResponse contentOutcome = null;
1722                if (compositeBroadcaster.hasHooks(thePointcut)) {
1723                        FullTextExtractionRequest contentRequest = new FullTextExtractionRequest(
1724                                        theEntity.getIdType(myContext), theResource, getResourceName(), theContentSupplier);
1725                        HookParams contentParams = new HookParams().add(FullTextExtractionRequest.class, contentRequest);
1726                        contentOutcome = (FullTextExtractionResponse)
1727                                        compositeBroadcaster.callHooksAndReturnObject(thePointcut, contentParams);
1728                }
1729
1730                if (contentOutcome == null || contentOutcome.isIndexNormally()) {
1731                        theEntityIndexSetter.accept(theContentSupplier.get());
1732                        theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_ALL);
1733                } else if (!contentOutcome.isDoNotIndex()) {
1734                        theEntityIndexSetter.accept(contentOutcome.getPayload());
1735                        theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_ALL);
1736                }
1737        }
1738
1739        @VisibleForTesting
1740        public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) {
1741                myPartitionSettings = thePartitionSettings;
1742        }
1743
1744        /**
1745         * Do not call this method outside of unit tests
1746         */
1747        @VisibleForTesting
1748        public void setJpaStorageResourceParserForUnitTest(IJpaStorageResourceParser theJpaStorageResourceParser) {
1749                myJpaStorageResourceParser = theJpaStorageResourceParser;
1750        }
1751
1752        @VisibleForTesting
1753        public void setResourceTypeCacheSvc(IResourceTypeCacheSvc theResourceTypeCacheSvc) {
1754                myResourceTypeCacheSvc = theResourceTypeCacheSvc;
1755        }
1756
1757        @Nullable
1758        @SuppressWarnings("unchecked")
1759        public static String parseContentTextIntoWords(
1760                        @Nonnull FhirContext theContext, @Nullable IBaseResource theResource) {
1761                if (theResource == null) {
1762                        return null;
1763                }
1764
1765                Class<IPrimitiveType<String>> stringType = (Class<IPrimitiveType<String>>)
1766                                theContext.getElementDefinition("string").getImplementingClass();
1767
1768                StringBuilder retVal = new StringBuilder();
1769                List<IPrimitiveType<String>> childElements =
1770                                theContext.newTerser().getAllPopulatedChildElementsOfType(theResource, stringType);
1771                for (IPrimitiveType<String> nextType : childElements) {
1772                        if (stringType.equals(nextType.getClass())) {
1773                                String nextValue = nextType.getValueAsString();
1774                                if (isNotBlank(nextValue)) {
1775                                        retVal.append(nextValue.replace("\n", " ").replace("\r", " "));
1776                                        retVal.append("\n");
1777                                }
1778                        }
1779                }
1780                return retVal.toString();
1781        }
1782
1783        public static String decodeResource(byte[] theResourceBytes, ResourceEncodingEnum theResourceEncoding) {
1784                String resourceText = null;
1785                switch (theResourceEncoding) {
1786                        case JSON:
1787                                resourceText = new String(theResourceBytes, Charsets.UTF_8);
1788                                break;
1789                        case JSONC:
1790                                resourceText = GZipUtil.decompress(theResourceBytes);
1791                                break;
1792                        case DEL:
1793                        case ESR:
1794                                break;
1795                }
1796                return resourceText;
1797        }
1798
1799        @Nullable
1800        private static String parseNarrativeTextIntoWords(@Nullable IBaseResource theResource) {
1801                if (theResource == null) {
1802                        return null;
1803                }
1804                StringBuilder b = new StringBuilder();
1805                if (theResource instanceof IResource) {
1806                        IResource resource = (IResource) theResource;
1807                        List<XMLEvent> xmlEvents = XmlUtil.parse(resource.getText().getDiv().getValue());
1808                        if (xmlEvents != null) {
1809                                for (XMLEvent next : xmlEvents) {
1810                                        if (next.isCharacters()) {
1811                                                Characters characters = next.asCharacters();
1812                                                b.append(characters.getData()).append(" ");
1813                                        }
1814                                }
1815                        }
1816                } else if (theResource instanceof IDomainResource) {
1817                        IDomainResource resource = (IDomainResource) theResource;
1818                        try {
1819                                String divAsString = resource.getText().getDivAsString();
1820                                List<XMLEvent> xmlEvents = XmlUtil.parse(divAsString);
1821                                if (xmlEvents != null) {
1822                                        for (XMLEvent next : xmlEvents) {
1823                                                if (next.isCharacters()) {
1824                                                        Characters characters = next.asCharacters();
1825                                                        b.append(characters.getData()).append(" ");
1826                                                }
1827                                        }
1828                                }
1829                        } catch (Exception e) {
1830                                throw new DataFormatException(Msg.code(934) + "Unable to convert DIV to string", e);
1831                        }
1832                }
1833                return b.toString();
1834        }
1835
1836        @VisibleForTesting
1837        public static void setDisableIncrementOnUpdateForUnitTest(boolean theDisableIncrementOnUpdateForUnitTest) {
1838                ourDisableIncrementOnUpdateForUnitTest = theDisableIncrementOnUpdateForUnitTest;
1839        }
1840
1841        /**
1842         * Do not call this method outside of unit tests
1843         */
1844        @VisibleForTesting
1845        public static void setValidationDisabledForUnitTest(boolean theValidationDisabledForUnitTest) {
1846                ourValidationDisabledForUnitTest = theValidationDisabledForUnitTest;
1847        }
1848
1849        private enum CreateOrUpdateByMatch {
1850                CREATE,
1851                UPDATE
1852        }
1853
1854        private void setTargetResourceTypeIdForResourceLinks(Collection<ResourceLink> resourceLinks) {
1855                resourceLinks.stream()
1856                                .filter(link -> link.getTargetResourceType() != null)
1857                                .forEach(link -> link.setTargetResourceTypeId(
1858                                                myResourceTypeCacheSvc.getResourceTypeId(link.getTargetResourceType())));
1859        }
1860}