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 =
855                                        getConditionalCreateOrUpdateErrorMsg(theCreateOrUpdate, theIfNoneExist, theResource.fhirType());
856                        throw new InvalidRequestException(Msg.code(929) + errorMsg);
857                }
858        }
859
860        private String getConditionalCreateOrUpdateErrorMsg(
861                        CreateOrUpdateByMatch theCreateOrUpdate, String url, String resourceType) {
862                return String.format(
863                                "Failed to process conditional %s. "
864                                                + "The supplied resource %s of type %s did not satisfy the conditional URL.",
865                                theCreateOrUpdate.name().toLowerCase(), url, resourceType);
866        }
867
868        @SuppressWarnings("unchecked")
869        @Override
870        public ResourceTable updateEntity(
871                        RequestDetails theRequest,
872                        final IBaseResource theResource,
873                        IBasePersistedResource theEntity,
874                        Date theDeletedTimestampOrNull,
875                        boolean thePerformIndexing,
876                        boolean theUpdateVersion,
877                        TransactionDetails theTransactionDetails,
878                        boolean theForceUpdate,
879                        boolean theCreateNewHistoryEntry) {
880                Validate.notNull(theEntity, "entity must not be null");
881                Validate.isTrue(
882                                theDeletedTimestampOrNull != null || theResource != null,
883                                "Must have either a resource[%s] or a deleted timestamp[%s] for resource PID[%s]",
884                                theDeletedTimestampOrNull != null,
885                                theResource != null,
886                                theEntity.getPersistentId());
887
888                ourLog.debug("Starting entity update");
889
890                ResourceTable entity = (ResourceTable) theEntity;
891
892                /*
893                 * This should be the very first thing..
894                 */
895                if (theResource != null) {
896                        if (thePerformIndexing && theDeletedTimestampOrNull == null) {
897                                if (!ourValidationDisabledForUnitTest) {
898                                        validateResourceForStorage((T) theResource, entity);
899                                }
900                        }
901                        if (!StringUtils.isBlank(entity.getResourceType())) {
902                                String resourceType = myContext.getResourceType(theResource);
903                                // This is just a sanity check and should never actually fail.
904                                // We resolve the ID using IdLookupService, and there should be
905                                // no way to get it to give you a mismatched type for an ID.
906                                Validate.isTrue(resourceType.equals(entity.getResourceType()));
907                        }
908                }
909
910                if (entity.getPublished() == null) {
911                        ourLog.debug("Entity has published time: {}", theTransactionDetails.getTransactionDate());
912                        entity.setPublished(theTransactionDetails.getTransactionDate());
913                }
914
915                ResourceIndexedSearchParams existingParams = null;
916
917                ResourceIndexedSearchParams newParams = null;
918
919                EncodedResource changed;
920                if (theDeletedTimestampOrNull != null) {
921                        // DELETE
922
923                        entity.setDeleted(theDeletedTimestampOrNull);
924                        entity.setUpdated(theDeletedTimestampOrNull);
925                        changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
926
927                } else {
928
929                        // CREATE or UPDATE
930
931                        IdentityHashMap<ResourceTable, ResourceIndexedSearchParams> existingSearchParams =
932                                        getSearchParamsMapFromTransaction(theTransactionDetails);
933                        existingParams = existingSearchParams.get(entity);
934                        if (existingParams == null) {
935                                existingParams = ResourceIndexedSearchParams.withLists(entity);
936                                /*
937                                 * If we have lots of resource links, this proactively fetches the targets so
938                                 * that we don't look them up one-by-one when comparing the new set to the
939                                 * old set later on
940                                 */
941                                if (existingParams.getResourceLinks().size() >= 10) {
942                                        List<Long> allPids = existingParams.getResourceLinks().stream()
943                                                        .map(ResourceLink::getId)
944                                                        .collect(Collectors.toList());
945                                        new QueryChunker<Long>().chunk(allPids, chunkPids -> {
946                                                List<ResourceLink> targets = myResourceLinkDao.findByPidAndFetchTargetDetails(chunkPids);
947                                                ourLog.trace("Prefetched targets: {}", targets);
948                                        });
949                                }
950                                existingSearchParams.put(entity, existingParams);
951                        }
952                        entity.setDeleted(null);
953
954                        // TODO: is this IF statement always true? Try removing it
955                        if (thePerformIndexing || theEntity.getVersion() == 1) {
956
957                                newParams = ResourceIndexedSearchParams.withSets();
958
959                                RequestPartitionId requestPartitionId;
960                                if (!myPartitionSettings.isPartitioningEnabled()) {
961                                        requestPartitionId = RequestPartitionId.allPartitions();
962                                } else if (entity.getPartitionId() != null) {
963                                        requestPartitionId = entity.getPartitionId().toPartitionId();
964                                } else {
965                                        requestPartitionId = myPartitionSettings.getDefaultRequestPartitionId();
966                                }
967
968                                // Extract search params for resource
969                                mySearchParamWithInlineReferencesExtractor.populateFromResource(
970                                                requestPartitionId,
971                                                newParams,
972                                                theTransactionDetails,
973                                                entity,
974                                                theResource,
975                                                existingParams,
976                                                theRequest,
977                                                thePerformIndexing);
978
979                                if (CollectionUtils.isNotEmpty(newParams.myLinks)) {
980                                        setTargetResourceTypeIdForResourceLinks(newParams.myLinks);
981                                }
982
983                                // Actually persist the ResourceTable and ResourceHistoryTable entities
984                                changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
985
986                                if (theForceUpdate) {
987                                        changed.setChanged(true);
988                                }
989
990                                if (changed.isChanged()) {
991                                        checkConditionalMatch(
992                                                        entity, theUpdateVersion, theResource, thePerformIndexing, newParams, theRequest);
993
994                                        if (CURRENTLY_REINDEXING.get(theResource) != Boolean.TRUE) {
995                                                entity.setUpdated(theTransactionDetails.getTransactionDate());
996                                        }
997                                        newParams.populateResourceTableSearchParamsPresentFlags(entity);
998                                }
999
1000                        } else {
1001
1002                                entity.setUpdated(theTransactionDetails.getTransactionDate());
1003                                changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, false);
1004                        }
1005                }
1006
1007                if (changed != null && changed.isChanged()) {
1008                        populateFullTextFieldsAndSetEntityStatus(theRequest, myContext, theResource, entity, newParams);
1009                }
1010
1011                if (thePerformIndexing
1012                                && changed != null
1013                                && !changed.isChanged()
1014                                && !theForceUpdate
1015                                && myStorageSettings.isSuppressUpdatesWithNoChange()
1016                                && (entity.getVersion() > 1 || theUpdateVersion)) {
1017                        ourLog.debug(
1018                                        "Resource {} has not changed",
1019                                        entity.getIdDt().toUnqualified().getValue());
1020                        if (theResource != null) {
1021                                myJpaStorageResourceParser.updateResourceMetadata(entity, theResource);
1022                        }
1023                        entity.setUnchangedInCurrentOperation(true);
1024                        return entity;
1025                }
1026
1027                if (entity.getId().getId() != null && theUpdateVersion) {
1028                        entity.markVersionUpdatedInCurrentTransaction();
1029                }
1030
1031                /*
1032                 * Save the resource itself
1033                 */
1034                if (entity.getId().getId() == null) {
1035                        myEntityManager.persist(entity);
1036
1037                        if (entity.getFhirId() == null) {
1038                                entity.setFhirId(Long.toString(entity.getId().getId()));
1039                        }
1040
1041                        postPersist(entity, (T) theResource, theRequest);
1042
1043                } else if (entity.getDeleted() != null) {
1044                        entity = myEntityManager.merge(entity);
1045
1046                        postDelete(entity);
1047
1048                } else {
1049                        entity = myEntityManager.merge(entity);
1050
1051                        postUpdate(entity, (T) theResource, theRequest);
1052                }
1053
1054                if (theCreateNewHistoryEntry) {
1055                        createHistoryEntry(theRequest, theResource, entity, changed);
1056                }
1057
1058                /*
1059                 * Update the "search param present" table which is used for the
1060                 * ?foo:missing=true queries
1061                 *
1062                 * Note that we're only populating this for reference params
1063                 * because the index tables for all other types have a MISSING column
1064                 * right on them for handling the :missing queries. We can't use the
1065                 * index table for resource links (reference indexes) because we index
1066                 * those by path and not by parameter name.
1067                 */
1068                if (thePerformIndexing && newParams != null) {
1069                        AddRemoveCount presenceCount =
1070                                        mySearchParamPresenceSvc.updatePresence(entity, newParams.mySearchParamPresentEntities);
1071
1072                        // Interceptor broadcast: JPA_PERFTRACE_INFO
1073                        if (!presenceCount.isEmpty()) {
1074                                IInterceptorBroadcaster compositeBroadcaster =
1075                                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequest);
1076                                if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) {
1077                                        StorageProcessingMessage message = new StorageProcessingMessage();
1078                                        message.setMessage(
1079                                                        "For " + entity.getIdDt().toUnqualifiedVersionless().getValue() + " added "
1080                                                                        + presenceCount.getAddCount() + " and removed " + presenceCount.getRemoveCount()
1081                                                                        + " resource search parameter presence entries");
1082                                        HookParams params = new HookParams()
1083                                                        .add(RequestDetails.class, theRequest)
1084                                                        .addIfMatchesType(ServletRequestDetails.class, theRequest)
1085                                                        .add(StorageProcessingMessage.class, message);
1086                                        compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params);
1087                                }
1088                        }
1089                }
1090
1091                /*
1092                 * Indexing
1093                 */
1094                if (thePerformIndexing) {
1095                        if (newParams == null) {
1096                                myExpungeService.deleteAllSearchParams(entity.getPersistentId());
1097                                entity.clearAllParamsPopulated();
1098                        } else {
1099
1100                                // Synchronize search param indexes
1101                                AddRemoveCount searchParamAddRemoveCount =
1102                                                myDaoSearchParamSynchronizer.synchronizeSearchParamsToDatabase(
1103                                                                newParams, entity, existingParams);
1104
1105                                newParams.populateResourceTableParamCollections(entity);
1106
1107                                // Interceptor broadcast: JPA_PERFTRACE_INFO
1108                                if (!searchParamAddRemoveCount.isEmpty()) {
1109                                        IInterceptorBroadcaster compositeBroadcaster =
1110                                                        CompositeInterceptorBroadcaster.newCompositeBroadcaster(
1111                                                                        myInterceptorBroadcaster, theRequest);
1112                                        if (compositeBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO)) {
1113                                                StorageProcessingMessage message = new StorageProcessingMessage();
1114                                                message.setMessage("For "
1115                                                                + entity.getIdDt().toUnqualifiedVersionless().getValue() + " added "
1116                                                                + searchParamAddRemoveCount.getAddCount() + " and removed "
1117                                                                + searchParamAddRemoveCount.getRemoveCount()
1118                                                                + " resource search parameter index entries");
1119                                                HookParams params = new HookParams()
1120                                                                .add(RequestDetails.class, theRequest)
1121                                                                .addIfMatchesType(ServletRequestDetails.class, theRequest)
1122                                                                .add(StorageProcessingMessage.class, message);
1123                                                compositeBroadcaster.callHooks(Pointcut.JPA_PERFTRACE_INFO, params);
1124                                        }
1125                                }
1126
1127                                // Put the final set of search params into the transaction
1128                                getSearchParamsMapFromTransaction(theTransactionDetails).put(entity, newParams);
1129                        }
1130                }
1131
1132                if (theResource != null) {
1133                        myJpaStorageResourceParser.updateResourceMetadata(entity, theResource);
1134                }
1135
1136                return entity;
1137        }
1138
1139        private static IdentityHashMap<ResourceTable, ResourceIndexedSearchParams> getSearchParamsMapFromTransaction(
1140                        TransactionDetails theTransactionDetails) {
1141                return theTransactionDetails.getOrCreateUserData(
1142                                HapiTransactionService.XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS, IdentityHashMap::new);
1143        }
1144
1145        /**
1146         * Make sure that the match URL was actually appropriate for the supplied
1147         * resource, if so configured, or do it only for first version, since technically it
1148         * is possible (and legal) for someone to be using a conditional update
1149         * to match a resource and then update it in a way that it no longer
1150         * matches.
1151         */
1152        private void checkConditionalMatch(
1153                        ResourceTable theEntity,
1154                        boolean theUpdateVersion,
1155                        IBaseResource theResource,
1156                        boolean thePerformIndexing,
1157                        ResourceIndexedSearchParams theNewParams,
1158                        RequestDetails theRequest) {
1159
1160                if (!thePerformIndexing) {
1161                        return;
1162                }
1163
1164                if (theEntity.getCreatedByMatchUrl() == null && theEntity.getUpdatedByMatchUrl() == null) {
1165                        return;
1166                }
1167
1168                // version is not updated at this point, but could be pending for update, which we consider here
1169                long pendingVersion = theEntity.getVersion();
1170                if (theUpdateVersion && !theEntity.isVersionUpdatedInCurrentTransaction()) {
1171                        pendingVersion++;
1172                }
1173
1174                if (myStorageSettings.isPreventInvalidatingConditionalMatchCriteria() || pendingVersion <= 1L) {
1175                        String createOrUpdateUrl;
1176                        CreateOrUpdateByMatch createOrUpdate;
1177
1178                        if (theEntity.getCreatedByMatchUrl() != null) {
1179                                createOrUpdateUrl = theEntity.getCreatedByMatchUrl();
1180                                createOrUpdate = CreateOrUpdateByMatch.CREATE;
1181                        } else {
1182                                createOrUpdateUrl = theEntity.getUpdatedByMatchUrl();
1183                                createOrUpdate = CreateOrUpdateByMatch.UPDATE;
1184                        }
1185
1186                        verifyMatchUrlForConditionalCreateOrUpdate(
1187                                        createOrUpdate, theResource, createOrUpdateUrl, theNewParams, theRequest);
1188                }
1189        }
1190
1191        public IBasePersistedResource<?> updateHistoryEntity(
1192                        RequestDetails theRequest,
1193                        T theResource,
1194                        IBasePersistedResource<?> theEntity,
1195                        IBasePersistedResource<?> theHistoryEntity,
1196                        IIdType theResourceId,
1197                        TransactionDetails theTransactionDetails,
1198                        boolean isUpdatingCurrent) {
1199                Validate.notNull(theEntity);
1200                Validate.isTrue(
1201                                theResource != null,
1202                                "Must have either a resource[%s] for resource PID[%s]",
1203                                theResource != null,
1204                                theEntity.getPersistentId());
1205
1206                ourLog.debug("Starting history entity update");
1207                EncodedResource encodedResource = new EncodedResource();
1208                ResourceHistoryTable historyEntity;
1209
1210                if (isUpdatingCurrent) {
1211                        ResourceTable entity = (ResourceTable) theEntity;
1212
1213                        IBaseResource oldResource;
1214                        if (getStorageSettings().isMassIngestionMode()) {
1215                                oldResource = null;
1216                        } else {
1217                                oldResource = myJpaStorageResourceParser.toResource(entity, false);
1218                        }
1219
1220                        notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, true);
1221
1222                        ResourceTable savedEntity = updateEntity(
1223                                        theRequest, theResource, entity, null, true, false, theTransactionDetails, false, false);
1224                        // Have to call populate again for the encodedResource, since using createHistoryEntry() will cause version
1225                        // constraint failure, ie updating the same resource at the same time
1226                        encodedResource = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
1227                        // For some reason the current version entity is not attached until after using updateEntity
1228                        historyEntity = ((ResourceTable) readEntity(theResourceId, theRequest)).getCurrentVersionEntity();
1229
1230                        // Update version/lastUpdated so that interceptors see the correct version
1231                        myJpaStorageResourceParser.updateResourceMetadata(savedEntity, theResource);
1232                        // Populate the PID in the resource, so it is available to hooks
1233                        addPidToResource(savedEntity, theResource);
1234
1235                        if (!savedEntity.isUnchangedInCurrentOperation()) {
1236                                notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, false);
1237                        }
1238                } else {
1239                        historyEntity = (ResourceHistoryTable) theHistoryEntity;
1240                        if (!StringUtils.isBlank(historyEntity.getResourceType())) {
1241                                String resourceType = myContext.getResourceType(theResource);
1242                                if (!resourceType.equals(historyEntity.getResourceType())) {
1243                                        throw new UnprocessableEntityException(Msg.code(930) + "Existing resource ID["
1244                                                        + historyEntity.getIdDt().toUnqualifiedVersionless() + "] is of type["
1245                                                        + historyEntity.getResourceType()
1246                                                        + "] - Cannot update with [" + resourceType + "]");
1247                                }
1248                        }
1249
1250                        historyEntity.setDeleted(null);
1251
1252                        // Check if resource is the same
1253                        ResourceEncodingEnum encoding = myStorageSettings.getResourceEncoding();
1254                        List<String> excludeElements = new ArrayList<>(8);
1255                        getExcludedElements(historyEntity.getResourceType(), excludeElements, theResource.getMeta());
1256                        String encodedResourceString =
1257                                        myResourceHistoryCalculator.encodeResource(theResource, encoding, excludeElements);
1258                        byte[] resourceBinary = ResourceHistoryCalculator.getResourceBinary(encoding, encodedResourceString);
1259                        final boolean changed = myResourceHistoryCalculator.isResourceHistoryChanged(
1260                                        historyEntity, resourceBinary, encodedResourceString);
1261
1262                        historyEntity.setUpdated(theTransactionDetails.getTransactionDate());
1263
1264                        if (!changed && myStorageSettings.isSuppressUpdatesWithNoChange() && (historyEntity.getVersion() > 1)) {
1265                                ourLog.debug(
1266                                                "Resource {} has not changed",
1267                                                historyEntity.getIdDt().toUnqualified().getValue());
1268                                myJpaStorageResourceParser.updateResourceMetadata(historyEntity, theResource);
1269                                return historyEntity;
1270                        }
1271
1272                        myResourceHistoryCalculator.populateEncodedResource(
1273                                        encodedResource, encodedResourceString, resourceBinary, encoding);
1274                }
1275                /*
1276                 * Save the resource itself to the resourceHistoryTable
1277                 */
1278                historyEntity = myEntityManager.merge(historyEntity);
1279                historyEntity.setEncoding(encodedResource.getEncoding());
1280                historyEntity.setResource(encodedResource.getResourceBinary());
1281                historyEntity.setResourceTextVc(encodedResource.getResourceText());
1282                myResourceHistoryTableDao.save(historyEntity);
1283
1284                myJpaStorageResourceParser.updateResourceMetadata(historyEntity, theResource);
1285
1286                return historyEntity;
1287        }
1288
1289        private void populateEncodedResource(
1290                        EncodedResource encodedResource,
1291                        String encodedResourceString,
1292                        byte[] theResourceBinary,
1293                        ResourceEncodingEnum theEncoding) {
1294                encodedResource.setResourceText(encodedResourceString);
1295                encodedResource.setResourceBinary(theResourceBinary);
1296                encodedResource.setEncoding(theEncoding);
1297        }
1298
1299        private void createHistoryEntry(
1300                        RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity, EncodedResource theChanged) {
1301                boolean versionedTags =
1302                                getStorageSettings().getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.VERSIONED;
1303
1304                ResourceHistoryTable historyEntry = null;
1305                long resourceVersion = theEntity.getVersion();
1306                if (!myStorageSettings.isResourceDbHistoryEnabled() && resourceVersion > 1L) {
1307                        /*
1308                         * If we're not storing history, then just pull the current history
1309                         * table row and update it. Note that there is always a chance that
1310                         * this could return null if the current resourceVersion has been expunged
1311                         * in which case we'll still create a new one
1312                         */
1313                        historyEntry = myResourceHistoryTableDao.findForIdAndVersion(
1314                                        theEntity.getResourceId().toFk(), resourceVersion - 1);
1315                        if (historyEntry != null) {
1316                                theEntity.populateHistoryEntityVersionAndDates(historyEntry);
1317                                if (versionedTags && theEntity.isHasTags()) {
1318                                        for (ResourceTag next : theEntity.getTags()) {
1319                                                historyEntry.addTag(next.getTag());
1320                                        }
1321                                }
1322                        }
1323                }
1324
1325                /*
1326                 * This should basically always be null unless resource history
1327                 * is disabled on this server. In that case, we'll just be reusing
1328                 * the previous version entity.
1329                 */
1330                if (historyEntry == null) {
1331                        historyEntry = theEntity.toHistory(versionedTags && theEntity.getDeleted() == null);
1332                }
1333
1334                historyEntry.setEncoding(theChanged.getEncoding());
1335                historyEntry.setResource(theChanged.getResourceBinary());
1336                historyEntry.setResourceTextVc(theChanged.getResourceText());
1337
1338                ourLog.debug("Saving history entry ID[{}] for RES_ID[{}]", historyEntry.getId(), historyEntry.getResourceId());
1339                myEntityManager.persist(historyEntry);
1340                theEntity.setCurrentVersionEntity(historyEntry);
1341
1342                // Save resource source
1343                String source = null;
1344
1345                if (theResource != null) {
1346                        if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) {
1347                                IBaseMetaType meta = theResource.getMeta();
1348                                source = MetaUtil.getSource(myContext, meta);
1349                        }
1350                        if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) {
1351                                source = ((IBaseHasExtensions) theResource.getMeta())
1352                                                .getExtension().stream()
1353                                                                .filter(t -> HapiExtensions.EXT_META_SOURCE.equals(t.getUrl()))
1354                                                                .filter(t -> t.getValue() instanceof IPrimitiveType)
1355                                                                .map(t -> ((IPrimitiveType<?>) t.getValue()).getValueAsString())
1356                                                                .findFirst()
1357                                                                .orElse(null);
1358                        }
1359                }
1360
1361                String requestId = getRequestId(theRequest, source);
1362                source = MetaUtil.cleanProvenanceSourceUriOrEmpty(source);
1363
1364                boolean shouldStoreSource =
1365                                myStorageSettings.getStoreMetaSourceInformation().isStoreSourceUri();
1366                boolean shouldStoreRequestId =
1367                                myStorageSettings.getStoreMetaSourceInformation().isStoreRequestId();
1368                boolean haveSource = isNotBlank(source) && shouldStoreSource;
1369                boolean haveRequestId = isNotBlank(requestId) && shouldStoreRequestId;
1370                if (haveSource || haveRequestId) {
1371                        if (haveRequestId) {
1372                                String persistedRequestId = left(requestId, Constants.REQUEST_ID_LENGTH);
1373                                historyEntry.setRequestId(persistedRequestId);
1374                        }
1375                        if (haveSource) {
1376                                String persistedSource = left(source, ResourceHistoryTable.SOURCE_URI_LENGTH);
1377                                historyEntry.setSourceUri(persistedSource);
1378                        }
1379                        if (theResource != null) {
1380                                MetaUtil.populateResourceSource(
1381                                                myFhirContext,
1382                                                shouldStoreSource ? source : null,
1383                                                shouldStoreRequestId ? requestId : null,
1384                                                theResource);
1385                        }
1386                }
1387        }
1388
1389        private String getRequestId(RequestDetails theRequest, String theSource) {
1390                if (myStorageSettings.isPreserveRequestIdInResourceBody()) {
1391                        return StringUtils.substringAfter(theSource, "#");
1392                }
1393                return theRequest != null ? theRequest.getRequestId() : null;
1394        }
1395
1396        @Override
1397        public DaoMethodOutcome updateInternal(
1398                        RequestDetails theRequestDetails,
1399                        T theResource,
1400                        String theMatchUrl,
1401                        boolean thePerformIndexing,
1402                        boolean theForceUpdateVersion,
1403                        IBasePersistedResource theEntity,
1404                        IIdType theResourceId,
1405                        @Nullable IBaseResource theOldResource,
1406                        RestOperationTypeEnum theOperationType,
1407                        TransactionDetails theTransactionDetails) {
1408
1409                ResourceTable entity = (ResourceTable) theEntity;
1410
1411                // We'll update the resource ID with the correct version later but for
1412                // now at least set it to something useful for the interceptors
1413                theResource.setId(entity.getIdDt());
1414
1415                // Notify IServerOperationInterceptors about pre-action call
1416                notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, true);
1417
1418                entity.setUpdatedByMatchUrl(theMatchUrl);
1419
1420                // Perform update
1421                ResourceTable savedEntity = updateEntity(
1422                                theRequestDetails,
1423                                theResource,
1424                                entity,
1425                                null,
1426                                thePerformIndexing,
1427                                thePerformIndexing,
1428                                theTransactionDetails,
1429                                theForceUpdateVersion,
1430                                thePerformIndexing);
1431
1432                /*
1433                 * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
1434                 * we'll manually increase the version. This is important because we want the updated version number
1435                 * to be reflected in the resource shared with interceptors
1436                 */
1437                if (!thePerformIndexing
1438                                && !savedEntity.isUnchangedInCurrentOperation()
1439                                && !ourDisableIncrementOnUpdateForUnitTest) {
1440                        if (!theResourceId.hasVersionIdPart()) {
1441                                theResourceId = theResourceId.withVersion(Long.toString(savedEntity.getVersion()));
1442                        }
1443                        incrementId(theResource, savedEntity, theResourceId);
1444                }
1445
1446                // Update version/lastUpdated so that interceptors see the correct version
1447                myJpaStorageResourceParser.updateResourceMetadata(savedEntity, theResource);
1448
1449                // Populate the PID in the resource so it is available to hooks
1450                addPidToResource(savedEntity, theResource);
1451
1452                // Notify interceptors
1453                if (!savedEntity.isUnchangedInCurrentOperation()) {
1454                        notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, false);
1455                }
1456
1457                Collection<? extends BaseTag> tagList = Collections.emptyList();
1458                if (entity.isHasTags()) {
1459                        tagList = entity.getTags();
1460                }
1461                long version = entity.getVersion();
1462                myJpaStorageResourceParser.populateResourceMetadata(entity, false, tagList, version, theResource);
1463
1464                boolean wasDeleted = false;
1465                if (theOldResource != null) {
1466                        wasDeleted = theOldResource.isDeleted();
1467                }
1468
1469                if (wasDeleted && !myStorageSettings.isDeleteEnabled()) {
1470                        String msg = myContext.getLocalizer().getMessage(BaseHapiFhirDao.class, "cantUndeleteWithDeletesDisabled");
1471                        throw new InvalidRequestException(Msg.code(2573) + msg);
1472                }
1473
1474                DaoMethodOutcome outcome = toMethodOutcome(
1475                                                theRequestDetails, savedEntity, theResource, theMatchUrl, theOperationType)
1476                                .setCreated(wasDeleted);
1477
1478                if (!thePerformIndexing) {
1479                        IIdType id = getContext().getVersion().newIdType();
1480                        id.setValue(entity.getIdDt().getValue());
1481                        outcome.setId(id);
1482                }
1483
1484                // Only include a task timer if we're not in a sub-request (i.e. a transaction)
1485                // since individual item times don't actually make much sense in the context
1486                // of a transaction
1487                StopWatch w = null;
1488                if (theRequestDetails != null && !theRequestDetails.isSubRequest()) {
1489                        if (theTransactionDetails != null && !theTransactionDetails.isFhirTransaction()) {
1490                                w = new StopWatch(theTransactionDetails.getTransactionDate());
1491                        }
1492                }
1493
1494                populateOperationOutcomeForUpdate(w, outcome, theMatchUrl, outcome.getOperationType(), theTransactionDetails);
1495
1496                return outcome;
1497        }
1498
1499        private void notifyInterceptors(
1500                        RequestDetails theRequestDetails,
1501                        T theResource,
1502                        IBaseResource theOldResource,
1503                        TransactionDetails theTransactionDetails,
1504                        boolean isUnchanged) {
1505                Pointcut interceptorPointcut = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED;
1506
1507                HookParams hookParams = new HookParams()
1508                                .add(IBaseResource.class, theOldResource)
1509                                .add(IBaseResource.class, theResource)
1510                                .add(RequestDetails.class, theRequestDetails)
1511                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
1512                                .add(TransactionDetails.class, theTransactionDetails);
1513
1514                if (!isUnchanged) {
1515                        hookParams.add(
1516                                        InterceptorInvocationTimingEnum.class,
1517                                        theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
1518                        interceptorPointcut = Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED;
1519                }
1520
1521                doCallHooks(theTransactionDetails, theRequestDetails, interceptorPointcut, hookParams);
1522        }
1523
1524        protected void addPidToResource(IResourceLookup<JpaPid> theEntity, IBaseResource theResource) {
1525                if (theResource instanceof IAnyResource) {
1526                        IDao.RESOURCE_PID.put(theResource, theEntity.getPersistentId());
1527                } else if (theResource instanceof IResource) {
1528                        IDao.RESOURCE_PID.put(theResource, theEntity.getPersistentId());
1529                }
1530        }
1531
1532        private void validateChildReferenceTargetTypes(IBase theElement, String thePath) {
1533                if (theElement == null) {
1534                        return;
1535                }
1536                BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass());
1537                if (!(def instanceof BaseRuntimeElementCompositeDefinition)) {
1538                        return;
1539                }
1540
1541                BaseRuntimeElementCompositeDefinition<?> cdef = (BaseRuntimeElementCompositeDefinition<?>) def;
1542                for (BaseRuntimeChildDefinition nextChildDef : cdef.getChildren()) {
1543
1544                        List<IBase> values = nextChildDef.getAccessor().getValues(theElement);
1545                        if (values == null || values.isEmpty()) {
1546                                continue;
1547                        }
1548
1549                        String newPath = thePath + "." + nextChildDef.getElementName();
1550
1551                        for (IBase nextChild : values) {
1552                                validateChildReferenceTargetTypes(nextChild, newPath);
1553                        }
1554
1555                        if (nextChildDef instanceof RuntimeChildResourceDefinition) {
1556                                RuntimeChildResourceDefinition nextChildDefRes = (RuntimeChildResourceDefinition) nextChildDef;
1557                                Set<String> validTypes = new HashSet<>();
1558                                boolean allowAny = false;
1559                                for (Class<? extends IBaseResource> nextValidType : nextChildDefRes.getResourceTypes()) {
1560                                        if (nextValidType.isInterface()) {
1561                                                allowAny = true;
1562                                                break;
1563                                        }
1564                                        validTypes.add(getContext().getResourceType(nextValidType));
1565                                }
1566
1567                                if (allowAny) {
1568                                        continue;
1569                                }
1570
1571                                if (getStorageSettings().isEnforceReferenceTargetTypes()) {
1572                                        for (IBase nextChild : values) {
1573                                                IBaseReference nextRef = (IBaseReference) nextChild;
1574                                                IIdType referencedId = nextRef.getReferenceElement();
1575                                                if (!isBlank(referencedId.getResourceType())) {
1576                                                        if (!isLogicalReference(referencedId)) {
1577                                                                if (!referencedId.getValue().contains("?")) {
1578                                                                        if (!validTypes.contains(referencedId.getResourceType())) {
1579                                                                                throw new UnprocessableEntityException(Msg.code(931)
1580                                                                                                + "Invalid reference found at path '" + newPath + "'. Resource type '"
1581                                                                                                + referencedId.getResourceType() + "' is not valid for this path");
1582                                                                        }
1583                                                                }
1584                                                        }
1585                                                }
1586                                        }
1587                                }
1588                        }
1589                }
1590        }
1591
1592        protected void validateMetaCount(int theMetaCount) {
1593                if (myStorageSettings.getResourceMetaCountHardLimit() != null) {
1594                        if (theMetaCount > myStorageSettings.getResourceMetaCountHardLimit()) {
1595                                throw new UnprocessableEntityException(Msg.code(932) + "Resource contains " + theMetaCount
1596                                                + " meta entries (tag/profile/security label), maximum is "
1597                                                + myStorageSettings.getResourceMetaCountHardLimit());
1598                        }
1599                }
1600        }
1601
1602        /**
1603         * 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
1604         * "subsetted" tag and rejects resources which have it. Subclasses should call the superclass implementation to preserve this check.
1605         *
1606         * @param theResource     The resource that is about to be persisted
1607         * @param theEntityToSave TODO
1608         */
1609        protected void validateResourceForStorage(T theResource, ResourceTable theEntityToSave) {
1610                Object tag = null;
1611
1612                int totalMetaCount = 0;
1613
1614                if (theResource instanceof IResource) {
1615                        IResource res = (IResource) theResource;
1616                        TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(res);
1617                        if (tagList != null) {
1618                                tag = tagList.getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE);
1619                                totalMetaCount += tagList.size();
1620                        }
1621                        List<IdDt> profileList = ResourceMetadataKeyEnum.PROFILES.get(res);
1622                        if (profileList != null) {
1623                                totalMetaCount += profileList.size();
1624                        }
1625                } else {
1626                        IAnyResource res = (IAnyResource) theResource;
1627                        tag = res.getMeta().getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE);
1628                        totalMetaCount += res.getMeta().getTag().size();
1629                        totalMetaCount += res.getMeta().getProfile().size();
1630                        totalMetaCount += res.getMeta().getSecurity().size();
1631                }
1632
1633                if (tag != null) {
1634                        throw new UnprocessableEntityException(
1635                                        Msg.code(933)
1636                                                        + "Resource contains the 'subsetted' tag, and must not be stored as it may contain a subset of available data");
1637                }
1638
1639                if (getStorageSettings().isEnforceReferenceTargetTypes()) {
1640                        String resName = getContext().getResourceType(theResource);
1641                        validateChildReferenceTargetTypes(theResource, resName);
1642                }
1643
1644                validateMetaCount(totalMetaCount);
1645        }
1646
1647        @PostConstruct
1648        public void start() {}
1649
1650        @VisibleForTesting
1651        public void setStorageSettingsForUnitTest(JpaStorageSettings theStorageSettings) {
1652                myStorageSettings = theStorageSettings;
1653        }
1654
1655        /**
1656         * If configured to do so, extracts the FullText indexes for the given
1657         * entity. The {@link ResourceTable#setIndexStatus(EntityIndexStatusEnum) Index Status}
1658         * is updated to reflect whether fulltext indexing is being used on this entity.
1659         */
1660        private void populateFullTextFieldsAndSetEntityStatus(
1661                        RequestDetails theRequestDetails,
1662                        final FhirContext theContext,
1663                        final IBaseResource theResource,
1664                        ResourceTable theEntity,
1665                        ResourceIndexedSearchParams theNewParams) {
1666                if (myFulltextSearchSvc == null || myFulltextSearchSvc.isDisabled()) {
1667                        theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_RDBMS_ONLY);
1668                        return;
1669                }
1670
1671                // This will get changed if we end up setting either
1672                theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_RDBMS_ONLY);
1673
1674                // Standard FullText indexing
1675                if (myStorageSettings.isHibernateSearchIndexFullText()) {
1676
1677                        // _content
1678                        if (mySearchParamRegistry.hasActiveSearchParam(
1679                                        theEntity.getResourceType(),
1680                                        Constants.PARAM_CONTENT,
1681                                        ISearchParamRegistry.SearchParamLookupContextEnum.INDEX)) {
1682                                Supplier<String> contentSupplier = () -> parseContentTextIntoWords(theContext, theResource);
1683                                Pointcut pointcut = Pointcut.JPA_INDEX_EXTRACT_FULLTEXT_CONTENT;
1684                                Consumer<String> contentEntitySetter = theEntity::setContentText;
1685                                extractFullTextIndexData(
1686                                                theRequestDetails, theResource, theEntity, pointcut, contentSupplier, contentEntitySetter);
1687                        }
1688
1689                        // _text
1690                        if (mySearchParamRegistry.hasActiveSearchParam(
1691                                        theEntity.getResourceType(),
1692                                        Constants.PARAM_TEXT,
1693                                        ISearchParamRegistry.SearchParamLookupContextEnum.INDEX)) {
1694                                Supplier<String> textSupplier = () -> parseNarrativeTextIntoWords(theResource);
1695                                Pointcut pointcut = Pointcut.JPA_INDEX_EXTRACT_FULLTEXT_TEXT;
1696                                Consumer<String> textEntitySetter = theEntity::setNarrativeText;
1697                                extractFullTextIndexData(
1698                                                theRequestDetails, theResource, theEntity, pointcut, textSupplier, textEntitySetter);
1699                        }
1700                }
1701
1702                // Advanced indexing - Index standard search params in the FullText index
1703                if (myStorageSettings.isHibernateSearchIndexSearchParams()) {
1704                        if (theResource != null) {
1705                                ExtendedHSearchIndexData hSearchIndexData =
1706                                                myFulltextSearchSvc.extractLuceneIndexData(theResource, theEntity, theNewParams);
1707                                theEntity.setLuceneIndexData(hSearchIndexData);
1708                        } else {
1709                                theEntity.setLuceneIndexData(null);
1710                        }
1711                        theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_ALL);
1712                }
1713        }
1714
1715        private void extractFullTextIndexData(
1716                        RequestDetails theRequestDetails,
1717                        IBaseResource theResource,
1718                        ResourceTable theEntity,
1719                        Pointcut thePointcut,
1720                        Supplier<String> theContentSupplier,
1721                        Consumer<String> theEntityIndexSetter) {
1722                IInterceptorBroadcaster compositeBroadcaster =
1723                                CompositeInterceptorBroadcaster.newCompositeBroadcaster(myInterceptorBroadcaster, theRequestDetails);
1724                FullTextExtractionResponse contentOutcome = null;
1725                if (compositeBroadcaster.hasHooks(thePointcut)) {
1726                        FullTextExtractionRequest contentRequest = new FullTextExtractionRequest(
1727                                        theEntity.getIdType(myContext), theResource, getResourceName(), theContentSupplier);
1728                        HookParams contentParams = new HookParams().add(FullTextExtractionRequest.class, contentRequest);
1729                        contentOutcome = (FullTextExtractionResponse)
1730                                        compositeBroadcaster.callHooksAndReturnObject(thePointcut, contentParams);
1731                }
1732
1733                if (contentOutcome == null || contentOutcome.isIndexNormally()) {
1734                        theEntityIndexSetter.accept(theContentSupplier.get());
1735                        theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_ALL);
1736                } else if (!contentOutcome.isDoNotIndex()) {
1737                        theEntityIndexSetter.accept(contentOutcome.getPayload());
1738                        theEntity.setIndexStatus(EntityIndexStatusEnum.INDEXED_ALL);
1739                }
1740        }
1741
1742        @VisibleForTesting
1743        public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) {
1744                myPartitionSettings = thePartitionSettings;
1745        }
1746
1747        /**
1748         * Do not call this method outside of unit tests
1749         */
1750        @VisibleForTesting
1751        public void setJpaStorageResourceParserForUnitTest(IJpaStorageResourceParser theJpaStorageResourceParser) {
1752                myJpaStorageResourceParser = theJpaStorageResourceParser;
1753        }
1754
1755        @VisibleForTesting
1756        public void setResourceTypeCacheSvc(IResourceTypeCacheSvc theResourceTypeCacheSvc) {
1757                myResourceTypeCacheSvc = theResourceTypeCacheSvc;
1758        }
1759
1760        @Nullable
1761        @SuppressWarnings("unchecked")
1762        public static String parseContentTextIntoWords(
1763                        @Nonnull FhirContext theContext, @Nullable IBaseResource theResource) {
1764                if (theResource == null) {
1765                        return null;
1766                }
1767
1768                Class<IPrimitiveType<String>> stringType = (Class<IPrimitiveType<String>>)
1769                                theContext.getElementDefinition("string").getImplementingClass();
1770
1771                StringBuilder retVal = new StringBuilder();
1772                List<IPrimitiveType<String>> childElements =
1773                                theContext.newTerser().getAllPopulatedChildElementsOfType(theResource, stringType);
1774                for (IPrimitiveType<String> nextType : childElements) {
1775                        if (stringType.equals(nextType.getClass())) {
1776                                String nextValue = nextType.getValueAsString();
1777                                if (isNotBlank(nextValue)) {
1778                                        retVal.append(nextValue.replace("\n", " ").replace("\r", " "));
1779                                        retVal.append("\n");
1780                                }
1781                        }
1782                }
1783                return retVal.toString();
1784        }
1785
1786        public static String decodeResource(byte[] theResourceBytes, ResourceEncodingEnum theResourceEncoding) {
1787                String resourceText = null;
1788                switch (theResourceEncoding) {
1789                        case JSON:
1790                                resourceText = new String(theResourceBytes, Charsets.UTF_8);
1791                                break;
1792                        case JSONC:
1793                                resourceText = GZipUtil.decompress(theResourceBytes);
1794                                break;
1795                        case DEL:
1796                        case ESR:
1797                                break;
1798                }
1799                return resourceText;
1800        }
1801
1802        @Nullable
1803        private static String parseNarrativeTextIntoWords(@Nullable IBaseResource theResource) {
1804                if (theResource == null) {
1805                        return null;
1806                }
1807                StringBuilder b = new StringBuilder();
1808                if (theResource instanceof IResource) {
1809                        IResource resource = (IResource) theResource;
1810                        List<XMLEvent> xmlEvents = XmlUtil.parse(resource.getText().getDiv().getValue());
1811                        if (xmlEvents != null) {
1812                                for (XMLEvent next : xmlEvents) {
1813                                        if (next.isCharacters()) {
1814                                                Characters characters = next.asCharacters();
1815                                                b.append(characters.getData()).append(" ");
1816                                        }
1817                                }
1818                        }
1819                } else if (theResource instanceof IDomainResource) {
1820                        IDomainResource resource = (IDomainResource) theResource;
1821                        try {
1822                                String divAsString = resource.getText().getDivAsString();
1823                                List<XMLEvent> xmlEvents = XmlUtil.parse(divAsString);
1824                                if (xmlEvents != null) {
1825                                        for (XMLEvent next : xmlEvents) {
1826                                                if (next.isCharacters()) {
1827                                                        Characters characters = next.asCharacters();
1828                                                        b.append(characters.getData()).append(" ");
1829                                                }
1830                                        }
1831                                }
1832                        } catch (Exception e) {
1833                                throw new DataFormatException(Msg.code(934) + "Unable to convert DIV to string", e);
1834                        }
1835                }
1836                return b.toString();
1837        }
1838
1839        @VisibleForTesting
1840        public static void setDisableIncrementOnUpdateForUnitTest(boolean theDisableIncrementOnUpdateForUnitTest) {
1841                ourDisableIncrementOnUpdateForUnitTest = theDisableIncrementOnUpdateForUnitTest;
1842        }
1843
1844        /**
1845         * Do not call this method outside of unit tests
1846         */
1847        @VisibleForTesting
1848        public static void setValidationDisabledForUnitTest(boolean theValidationDisabledForUnitTest) {
1849                ourValidationDisabledForUnitTest = theValidationDisabledForUnitTest;
1850        }
1851
1852        private enum CreateOrUpdateByMatch {
1853                CREATE,
1854                UPDATE
1855        }
1856
1857        private void setTargetResourceTypeIdForResourceLinks(Collection<ResourceLink> resourceLinks) {
1858                resourceLinks.stream()
1859                                .filter(link -> link.getTargetResourceType() != null)
1860                                .forEach(link -> link.setTargetResourceTypeId(
1861                                                myResourceTypeCacheSvc.getResourceTypeId(link.getTargetResourceType())));
1862        }
1863}