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