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