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                                // Synchronize composite params
1289                                mySearchParamWithInlineReferencesExtractor.storeUniqueComboParameters(
1290                                                newParams, entity, existingParams);
1291                        }
1292                }
1293
1294                if (theResource != null) {
1295                        myJpaStorageResourceParser.updateResourceMetadata(entity, theResource);
1296                }
1297
1298                return entity;
1299        }
1300
1301        /**
1302         * Make sure that the match URL was actually appropriate for the supplied
1303         * resource, if so configured, or do it only for first version, since technically it
1304         * is possible (and legal) for someone to be using a conditional update
1305         * to match a resource and then update it in a way that it no longer
1306         * matches.
1307         */
1308        private void checkConditionalMatch(
1309                        ResourceTable theEntity,
1310                        boolean theUpdateVersion,
1311                        IBaseResource theResource,
1312                        boolean thePerformIndexing,
1313                        ResourceIndexedSearchParams theNewParams,
1314                        RequestDetails theRequest) {
1315
1316                if (!thePerformIndexing) {
1317                        return;
1318                }
1319
1320                if (theEntity.getCreatedByMatchUrl() == null && theEntity.getUpdatedByMatchUrl() == null) {
1321                        return;
1322                }
1323
1324                // version is not updated at this point, but could be pending for update, which we consider here
1325                long pendingVersion = theEntity.getVersion();
1326                if (theUpdateVersion && !theEntity.isVersionUpdatedInCurrentTransaction()) {
1327                        pendingVersion++;
1328                }
1329
1330                if (myStorageSettings.isPreventInvalidatingConditionalMatchCriteria() || pendingVersion <= 1L) {
1331                        String createOrUpdateUrl;
1332                        CreateOrUpdateByMatch createOrUpdate;
1333
1334                        if (theEntity.getCreatedByMatchUrl() != null) {
1335                                createOrUpdateUrl = theEntity.getCreatedByMatchUrl();
1336                                createOrUpdate = CreateOrUpdateByMatch.CREATE;
1337                        } else {
1338                                createOrUpdateUrl = theEntity.getUpdatedByMatchUrl();
1339                                createOrUpdate = CreateOrUpdateByMatch.UPDATE;
1340                        }
1341
1342                        verifyMatchUrlForConditionalCreateOrUpdate(
1343                                        createOrUpdate, theResource, createOrUpdateUrl, theNewParams, theRequest);
1344                }
1345        }
1346
1347        public IBasePersistedResource updateHistoryEntity(
1348                        RequestDetails theRequest,
1349                        T theResource,
1350                        IBasePersistedResource theEntity,
1351                        IBasePersistedResource theHistoryEntity,
1352                        IIdType theResourceId,
1353                        TransactionDetails theTransactionDetails,
1354                        boolean isUpdatingCurrent) {
1355                Validate.notNull(theEntity);
1356                Validate.isTrue(
1357                                theResource != null,
1358                                "Must have either a resource[%s] for resource PID[%s]",
1359                                theResource != null,
1360                                theEntity.getPersistentId());
1361
1362                ourLog.debug("Starting history entity update");
1363                EncodedResource encodedResource = new EncodedResource();
1364                ResourceHistoryTable historyEntity;
1365
1366                if (isUpdatingCurrent) {
1367                        ResourceTable entity = (ResourceTable) theEntity;
1368
1369                        IBaseResource oldResource;
1370                        if (getStorageSettings().isMassIngestionMode()) {
1371                                oldResource = null;
1372                        } else {
1373                                oldResource = myJpaStorageResourceParser.toResource(entity, false);
1374                        }
1375
1376                        notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, true);
1377
1378                        ResourceTable savedEntity = updateEntity(
1379                                        theRequest, theResource, entity, null, true, false, theTransactionDetails, false, false);
1380                        // Have to call populate again for the encodedResource, since using createHistoryEntry() will cause version
1381                        // constraint failure, ie updating the same resource at the same time
1382                        encodedResource = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
1383                        // For some reason the current version entity is not attached until after using updateEntity
1384                        historyEntity = ((ResourceTable) readEntity(theResourceId, theRequest)).getCurrentVersionEntity();
1385
1386                        // Update version/lastUpdated so that interceptors see the correct version
1387                        myJpaStorageResourceParser.updateResourceMetadata(savedEntity, theResource);
1388                        // Populate the PID in the resource, so it is available to hooks
1389                        addPidToResource(savedEntity, theResource);
1390
1391                        if (!savedEntity.isUnchangedInCurrentOperation()) {
1392                                notifyInterceptors(theRequest, theResource, oldResource, theTransactionDetails, false);
1393                        }
1394                } else {
1395                        historyEntity = (ResourceHistoryTable) theHistoryEntity;
1396                        if (!StringUtils.isBlank(historyEntity.getResourceType())) {
1397                                validateIncomingResourceTypeMatchesExisting(theResource, historyEntity);
1398                        }
1399
1400                        historyEntity.setDeleted(null);
1401
1402                        // Check if resource is the same
1403                        ResourceEncodingEnum encoding = myStorageSettings.getResourceEncoding();
1404                        List<String> excludeElements = new ArrayList<>(8);
1405                        getExcludedElements(historyEntity.getResourceType(), excludeElements, theResource.getMeta());
1406                        String encodedResourceString =
1407                                        myResourceHistoryCalculator.encodeResource(theResource, encoding, excludeElements);
1408                        byte[] resourceBinary = ResourceHistoryCalculator.getResourceBinary(encoding, encodedResourceString);
1409                        final boolean changed = myResourceHistoryCalculator.isResourceHistoryChanged(
1410                                        historyEntity, resourceBinary, encodedResourceString);
1411
1412                        historyEntity.setUpdated(theTransactionDetails.getTransactionDate());
1413
1414                        if (!changed && myStorageSettings.isSuppressUpdatesWithNoChange() && (historyEntity.getVersion() > 1)) {
1415                                ourLog.debug(
1416                                                "Resource {} has not changed",
1417                                                historyEntity.getIdDt().toUnqualified().getValue());
1418                                myJpaStorageResourceParser.updateResourceMetadata(historyEntity, theResource);
1419                                return historyEntity;
1420                        }
1421
1422                        myResourceHistoryCalculator.populateEncodedResource(
1423                                        encodedResource, encodedResourceString, resourceBinary, encoding);
1424                }
1425                /*
1426                 * Save the resource itself to the resourceHistoryTable
1427                 */
1428                historyEntity = myEntityManager.merge(historyEntity);
1429                historyEntity.setEncoding(encodedResource.getEncoding());
1430                historyEntity.setResource(encodedResource.getResourceBinary());
1431                historyEntity.setResourceTextVc(encodedResource.getResourceText());
1432                myResourceHistoryTableDao.save(historyEntity);
1433
1434                myJpaStorageResourceParser.updateResourceMetadata(historyEntity, theResource);
1435
1436                return historyEntity;
1437        }
1438
1439        private void populateEncodedResource(
1440                        EncodedResource encodedResource,
1441                        String encodedResourceString,
1442                        byte[] theResourceBinary,
1443                        ResourceEncodingEnum theEncoding) {
1444                encodedResource.setResourceText(encodedResourceString);
1445                encodedResource.setResourceBinary(theResourceBinary);
1446                encodedResource.setEncoding(theEncoding);
1447        }
1448
1449        /**
1450         * TODO eventually consider refactoring this to be part of an interceptor.
1451         * <p>
1452         * Throws an exception if the partition of the request, and the partition of the existing entity do not match.
1453         *
1454         * @param theRequest the request.
1455         * @param entity     the existing entity.
1456         */
1457        private void failIfPartitionMismatch(RequestDetails theRequest, ResourceTable entity) {
1458                if (myPartitionSettings.isPartitioningEnabled()
1459                                && theRequest != null
1460                                && theRequest.getTenantId() != null
1461                                && entity.getPartitionId() != null) {
1462                        PartitionEntity partitionEntity = myPartitionLookupSvc.getPartitionByName(theRequest.getTenantId());
1463                        // partitionEntity should never be null
1464                        if (partitionEntity != null
1465                                        && !partitionEntity.getId().equals(entity.getPartitionId().getPartitionId())) {
1466                                throw new InvalidRequestException(Msg.code(2079) + "Resource " + entity.getResourceType() + "/"
1467                                                + entity.getId() + " is not known");
1468                        }
1469                }
1470        }
1471
1472        private void createHistoryEntry(
1473                        RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity, EncodedResource theChanged) {
1474                boolean versionedTags =
1475                                getStorageSettings().getTagStorageMode() == JpaStorageSettings.TagStorageModeEnum.VERSIONED;
1476
1477                ResourceHistoryTable historyEntry = null;
1478                long resourceVersion = theEntity.getVersion();
1479                boolean reusingHistoryEntity = false;
1480                if (!myStorageSettings.isResourceDbHistoryEnabled() && resourceVersion > 1L) {
1481                        /*
1482                         * If we're not storing history, then just pull the current history
1483                         * table row and update it. Note that there is always a chance that
1484                         * this could return null if the current resourceVersion has been expunged
1485                         * in which case we'll still create a new one
1486                         */
1487                        historyEntry = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(
1488                                        theEntity.getResourceId(), resourceVersion - 1);
1489                        if (historyEntry != null) {
1490                                reusingHistoryEntity = true;
1491                                theEntity.populateHistoryEntityVersionAndDates(historyEntry);
1492                                if (versionedTags && theEntity.isHasTags()) {
1493                                        for (ResourceTag next : theEntity.getTags()) {
1494                                                historyEntry.addTag(next.getTag());
1495                                        }
1496                                }
1497                        }
1498                }
1499
1500                /*
1501                 * This should basically always be null unless resource history
1502                 * is disabled on this server. In that case, we'll just be reusing
1503                 * the previous version entity.
1504                 */
1505                if (historyEntry == null) {
1506                        historyEntry = theEntity.toHistory(versionedTags);
1507                }
1508
1509                historyEntry.setEncoding(theChanged.getEncoding());
1510                historyEntry.setResource(theChanged.getResourceBinary());
1511                historyEntry.setResourceTextVc(theChanged.getResourceText());
1512
1513                ourLog.debug("Saving history entry ID[{}] for RES_ID[{}]", historyEntry.getId(), historyEntry.getResourceId());
1514                myResourceHistoryTableDao.save(historyEntry);
1515                theEntity.setCurrentVersionEntity(historyEntry);
1516
1517                // Save resource source
1518                String source = null;
1519
1520                if (theResource != null) {
1521                        if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) {
1522                                IBaseMetaType meta = theResource.getMeta();
1523                                source = MetaUtil.getSource(myContext, meta);
1524                        }
1525                        if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) {
1526                                source = ((IBaseHasExtensions) theResource.getMeta())
1527                                                .getExtension().stream()
1528                                                                .filter(t -> HapiExtensions.EXT_META_SOURCE.equals(t.getUrl()))
1529                                                                .filter(t -> t.getValue() instanceof IPrimitiveType)
1530                                                                .map(t -> ((IPrimitiveType<?>) t.getValue()).getValueAsString())
1531                                                                .findFirst()
1532                                                                .orElse(null);
1533                        }
1534                }
1535
1536                String requestId = getRequestId(theRequest, source);
1537                source = MetaUtil.cleanProvenanceSourceUriOrEmpty(source);
1538
1539                boolean shouldStoreSource =
1540                                myStorageSettings.getStoreMetaSourceInformation().isStoreSourceUri();
1541                boolean shouldStoreRequestId =
1542                                myStorageSettings.getStoreMetaSourceInformation().isStoreRequestId();
1543                boolean haveSource = isNotBlank(source) && shouldStoreSource;
1544                boolean haveRequestId = isNotBlank(requestId) && shouldStoreRequestId;
1545                if (haveSource || haveRequestId) {
1546                        ResourceHistoryProvenanceEntity provenance = null;
1547                        if (reusingHistoryEntity) {
1548                                /*
1549                                 * If version history is disabled, then we may be reusing
1550                                 * a previous history entity. If that's the case, let's try
1551                                 * to reuse the previous provenance entity too.
1552                                 */
1553                                provenance = historyEntry.getProvenance();
1554                        }
1555                        if (provenance == null) {
1556                                provenance = historyEntry.toProvenance();
1557                        }
1558                        provenance.setResourceHistoryTable(historyEntry);
1559                        provenance.setResourceTable(theEntity);
1560                        provenance.setPartitionId(theEntity.getPartitionId());
1561                        if (haveRequestId) {
1562                                String persistedRequestId = left(requestId, Constants.REQUEST_ID_LENGTH);
1563                                provenance.setRequestId(persistedRequestId);
1564                                historyEntry.setRequestId(persistedRequestId);
1565                        }
1566                        if (haveSource) {
1567                                String persistedSource = left(source, ResourceHistoryTable.SOURCE_URI_LENGTH);
1568                                provenance.setSourceUri(persistedSource);
1569                                historyEntry.setSourceUri(persistedSource);
1570                        }
1571                        if (theResource != null) {
1572                                MetaUtil.populateResourceSource(
1573                                                myFhirContext,
1574                                                shouldStoreSource ? source : null,
1575                                                shouldStoreRequestId ? requestId : null,
1576                                                theResource);
1577                        }
1578
1579                        myEntityManager.persist(provenance);
1580                }
1581        }
1582
1583        private String getRequestId(RequestDetails theRequest, String theSource) {
1584                if (myStorageSettings.isPreserveRequestIdInResourceBody()) {
1585                        return StringUtils.substringAfter(theSource, "#");
1586                }
1587                return theRequest != null ? theRequest.getRequestId() : null;
1588        }
1589
1590        private void validateIncomingResourceTypeMatchesExisting(IBaseResource theResource, BaseHasResource entity) {
1591                String resourceType = myContext.getResourceType(theResource);
1592                if (!resourceType.equals(entity.getResourceType())) {
1593                        throw new UnprocessableEntityException(Msg.code(930) + "Existing resource ID["
1594                                        + entity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + entity.getResourceType()
1595                                        + "] - Cannot update with [" + resourceType + "]");
1596                }
1597        }
1598
1599        @Override
1600        public DaoMethodOutcome updateInternal(
1601                        RequestDetails theRequestDetails,
1602                        T theResource,
1603                        String theMatchUrl,
1604                        boolean thePerformIndexing,
1605                        boolean theForceUpdateVersion,
1606                        IBasePersistedResource theEntity,
1607                        IIdType theResourceId,
1608                        @Nullable IBaseResource theOldResource,
1609                        RestOperationTypeEnum theOperationType,
1610                        TransactionDetails theTransactionDetails) {
1611
1612                ResourceTable entity = (ResourceTable) theEntity;
1613
1614                // We'll update the resource ID with the correct version later but for
1615                // now at least set it to something useful for the interceptors
1616                theResource.setId(entity.getIdDt());
1617
1618                // Notify IServerOperationInterceptors about pre-action call
1619                notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, true);
1620
1621                entity.setUpdatedByMatchUrl(theMatchUrl);
1622
1623                // Perform update
1624                ResourceTable savedEntity = updateEntity(
1625                                theRequestDetails,
1626                                theResource,
1627                                entity,
1628                                null,
1629                                thePerformIndexing,
1630                                thePerformIndexing,
1631                                theTransactionDetails,
1632                                theForceUpdateVersion,
1633                                thePerformIndexing);
1634
1635                /*
1636                 * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
1637                 * we'll manually increase the version. This is important because we want the updated version number
1638                 * to be reflected in the resource shared with interceptors
1639                 */
1640                if (!thePerformIndexing
1641                                && !savedEntity.isUnchangedInCurrentOperation()
1642                                && !ourDisableIncrementOnUpdateForUnitTest) {
1643                        if (!theResourceId.hasVersionIdPart()) {
1644                                theResourceId = theResourceId.withVersion(Long.toString(savedEntity.getVersion()));
1645                        }
1646                        incrementId(theResource, savedEntity, theResourceId);
1647                }
1648
1649                // Update version/lastUpdated so that interceptors see the correct version
1650                myJpaStorageResourceParser.updateResourceMetadata(savedEntity, theResource);
1651
1652                // Populate the PID in the resource so it is available to hooks
1653                addPidToResource(savedEntity, theResource);
1654
1655                // Notify interceptors
1656                if (!savedEntity.isUnchangedInCurrentOperation()) {
1657                        notifyInterceptors(theRequestDetails, theResource, theOldResource, theTransactionDetails, false);
1658                }
1659
1660                Collection<? extends BaseTag> tagList = Collections.emptyList();
1661                if (entity.isHasTags()) {
1662                        tagList = entity.getTags();
1663                }
1664                long version = entity.getVersion();
1665                myJpaStorageResourceParser.populateResourceMetadata(entity, false, tagList, version, theResource);
1666
1667                boolean wasDeleted = false;
1668                if (theOldResource != null) {
1669                        wasDeleted = theOldResource.isDeleted();
1670                }
1671
1672                DaoMethodOutcome outcome = toMethodOutcome(
1673                                                theRequestDetails, savedEntity, theResource, theMatchUrl, theOperationType)
1674                                .setCreated(wasDeleted);
1675
1676                if (!thePerformIndexing) {
1677                        IIdType id = getContext().getVersion().newIdType();
1678                        id.setValue(entity.getIdDt().getValue());
1679                        outcome.setId(id);
1680                }
1681
1682                // Only include a task timer if we're not in a sub-request (i.e. a transaction)
1683                // since individual item times don't actually make much sense in the context
1684                // of a transaction
1685                StopWatch w = null;
1686                if (theRequestDetails != null && !theRequestDetails.isSubRequest()) {
1687                        if (theTransactionDetails != null && !theTransactionDetails.isFhirTransaction()) {
1688                                w = new StopWatch(theTransactionDetails.getTransactionDate());
1689                        }
1690                }
1691
1692                populateOperationOutcomeForUpdate(w, outcome, theMatchUrl, outcome.getOperationType());
1693
1694                return outcome;
1695        }
1696
1697        private void notifyInterceptors(
1698                        RequestDetails theRequestDetails,
1699                        T theResource,
1700                        IBaseResource theOldResource,
1701                        TransactionDetails theTransactionDetails,
1702                        boolean isUnchanged) {
1703                Pointcut interceptorPointcut = Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED;
1704
1705                HookParams hookParams = new HookParams()
1706                                .add(IBaseResource.class, theOldResource)
1707                                .add(IBaseResource.class, theResource)
1708                                .add(RequestDetails.class, theRequestDetails)
1709                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
1710                                .add(TransactionDetails.class, theTransactionDetails);
1711
1712                if (!isUnchanged) {
1713                        hookParams.add(
1714                                        InterceptorInvocationTimingEnum.class,
1715                                        theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
1716                        interceptorPointcut = Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED;
1717                }
1718
1719                doCallHooks(theTransactionDetails, theRequestDetails, interceptorPointcut, hookParams);
1720        }
1721
1722        protected void addPidToResource(IResourceLookup<JpaPid> theEntity, IBaseResource theResource) {
1723                if (theResource instanceof IAnyResource) {
1724                        IDao.RESOURCE_PID.put(theResource, theEntity.getPersistentId().getId());
1725                } else if (theResource instanceof IResource) {
1726                        IDao.RESOURCE_PID.put(theResource, theEntity.getPersistentId().getId());
1727                }
1728        }
1729
1730        private void validateChildReferenceTargetTypes(IBase theElement, String thePath) {
1731                if (theElement == null) {
1732                        return;
1733                }
1734                BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass());
1735                if (!(def instanceof BaseRuntimeElementCompositeDefinition)) {
1736                        return;
1737                }
1738
1739                BaseRuntimeElementCompositeDefinition<?> cdef = (BaseRuntimeElementCompositeDefinition<?>) def;
1740                for (BaseRuntimeChildDefinition nextChildDef : cdef.getChildren()) {
1741
1742                        List<IBase> values = nextChildDef.getAccessor().getValues(theElement);
1743                        if (values == null || values.isEmpty()) {
1744                                continue;
1745                        }
1746
1747                        String newPath = thePath + "." + nextChildDef.getElementName();
1748
1749                        for (IBase nextChild : values) {
1750                                validateChildReferenceTargetTypes(nextChild, newPath);
1751                        }
1752
1753                        if (nextChildDef instanceof RuntimeChildResourceDefinition) {
1754                                RuntimeChildResourceDefinition nextChildDefRes = (RuntimeChildResourceDefinition) nextChildDef;
1755                                Set<String> validTypes = new HashSet<>();
1756                                boolean allowAny = false;
1757                                for (Class<? extends IBaseResource> nextValidType : nextChildDefRes.getResourceTypes()) {
1758                                        if (nextValidType.isInterface()) {
1759                                                allowAny = true;
1760                                                break;
1761                                        }
1762                                        validTypes.add(getContext().getResourceType(nextValidType));
1763                                }
1764
1765                                if (allowAny) {
1766                                        continue;
1767                                }
1768
1769                                if (getStorageSettings().isEnforceReferenceTargetTypes()) {
1770                                        for (IBase nextChild : values) {
1771                                                IBaseReference nextRef = (IBaseReference) nextChild;
1772                                                IIdType referencedId = nextRef.getReferenceElement();
1773                                                if (!isBlank(referencedId.getResourceType())) {
1774                                                        if (!isLogicalReference(referencedId)) {
1775                                                                if (!referencedId.getValue().contains("?")) {
1776                                                                        if (!validTypes.contains(referencedId.getResourceType())) {
1777                                                                                throw new UnprocessableEntityException(Msg.code(931)
1778                                                                                                + "Invalid reference found at path '" + newPath + "'. Resource type '"
1779                                                                                                + referencedId.getResourceType() + "' is not valid for this path");
1780                                                                        }
1781                                                                }
1782                                                        }
1783                                                }
1784                                        }
1785                                }
1786                        }
1787                }
1788        }
1789
1790        protected void validateMetaCount(int theMetaCount) {
1791                if (myStorageSettings.getResourceMetaCountHardLimit() != null) {
1792                        if (theMetaCount > myStorageSettings.getResourceMetaCountHardLimit()) {
1793                                throw new UnprocessableEntityException(Msg.code(932) + "Resource contains " + theMetaCount
1794                                                + " meta entries (tag/profile/security label), maximum is "
1795                                                + myStorageSettings.getResourceMetaCountHardLimit());
1796                        }
1797                }
1798        }
1799
1800        /**
1801         * 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
1802         * "subsetted" tag and rejects resources which have it. Subclasses should call the superclass implementation to preserve this check.
1803         *
1804         * @param theResource     The resource that is about to be persisted
1805         * @param theEntityToSave TODO
1806         */
1807        protected void validateResourceForStorage(T theResource, ResourceTable theEntityToSave) {
1808                Object tag = null;
1809
1810                int totalMetaCount = 0;
1811
1812                if (theResource instanceof IResource) {
1813                        IResource res = (IResource) theResource;
1814                        TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(res);
1815                        if (tagList != null) {
1816                                tag = tagList.getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE);
1817                                totalMetaCount += tagList.size();
1818                        }
1819                        List<IdDt> profileList = ResourceMetadataKeyEnum.PROFILES.get(res);
1820                        if (profileList != null) {
1821                                totalMetaCount += profileList.size();
1822                        }
1823                } else {
1824                        IAnyResource res = (IAnyResource) theResource;
1825                        tag = res.getMeta().getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE);
1826                        totalMetaCount += res.getMeta().getTag().size();
1827                        totalMetaCount += res.getMeta().getProfile().size();
1828                        totalMetaCount += res.getMeta().getSecurity().size();
1829                }
1830
1831                if (tag != null) {
1832                        throw new UnprocessableEntityException(
1833                                        Msg.code(933)
1834                                                        + "Resource contains the 'subsetted' tag, and must not be stored as it may contain a subset of available data");
1835                }
1836
1837                if (getStorageSettings().isEnforceReferenceTargetTypes()) {
1838                        String resName = getContext().getResourceType(theResource);
1839                        validateChildReferenceTargetTypes(theResource, resName);
1840                }
1841
1842                validateMetaCount(totalMetaCount);
1843        }
1844
1845        @PostConstruct
1846        public void start() {
1847                // nothing yet
1848        }
1849
1850        @VisibleForTesting
1851        public void setStorageSettingsForUnitTest(JpaStorageSettings theStorageSettings) {
1852                myStorageSettings = theStorageSettings;
1853        }
1854
1855        public void populateFullTextFields(
1856                        final FhirContext theContext,
1857                        final IBaseResource theResource,
1858                        ResourceTable theEntity,
1859                        ResourceIndexedSearchParams theNewParams) {
1860                if (theEntity.getDeleted() != null) {
1861                        theEntity.setNarrativeText(null);
1862                        theEntity.setContentText(null);
1863                } else {
1864                        theEntity.setNarrativeText(parseNarrativeTextIntoWords(theResource));
1865                        theEntity.setContentText(parseContentTextIntoWords(theContext, theResource));
1866                        if (myStorageSettings.isAdvancedHSearchIndexing()) {
1867                                ExtendedHSearchIndexData hSearchIndexData =
1868                                                myFulltextSearchSvc.extractLuceneIndexData(theResource, theNewParams);
1869                                theEntity.setLuceneIndexData(hSearchIndexData);
1870                        }
1871                }
1872        }
1873
1874        @VisibleForTesting
1875        public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) {
1876                myPartitionSettings = thePartitionSettings;
1877        }
1878
1879        /**
1880         * Do not call this method outside of unit tests
1881         */
1882        @VisibleForTesting
1883        public void setJpaStorageResourceParserForUnitTest(IJpaStorageResourceParser theJpaStorageResourceParser) {
1884                myJpaStorageResourceParser = theJpaStorageResourceParser;
1885        }
1886
1887        private class AddTagDefinitionToCacheAfterCommitSynchronization implements TransactionSynchronization {
1888
1889                private final TagDefinition myTagDefinition;
1890                private final MemoryCacheService.TagDefinitionCacheKey myKey;
1891
1892                public AddTagDefinitionToCacheAfterCommitSynchronization(
1893                                MemoryCacheService.TagDefinitionCacheKey theKey, TagDefinition theTagDefinition) {
1894                        myTagDefinition = theTagDefinition;
1895                        myKey = theKey;
1896                }
1897
1898                @Override
1899                public void afterCommit() {
1900                        myMemoryCacheService.put(MemoryCacheService.CacheEnum.TAG_DEFINITION, myKey, myTagDefinition);
1901                }
1902        }
1903
1904        @Nonnull
1905        public static MemoryCacheService.TagDefinitionCacheKey toTagDefinitionMemoryCacheKey(
1906                        TagTypeEnum theTagType, String theScheme, String theTerm, String theVersion, Boolean theUserSelected) {
1907                return new MemoryCacheService.TagDefinitionCacheKey(
1908                                theTagType, theScheme, theTerm, theVersion, theUserSelected);
1909        }
1910
1911        @SuppressWarnings("unchecked")
1912        public static String parseContentTextIntoWords(FhirContext theContext, IBaseResource theResource) {
1913
1914                Class<IPrimitiveType<String>> stringType = (Class<IPrimitiveType<String>>)
1915                                theContext.getElementDefinition("string").getImplementingClass();
1916
1917                StringBuilder retVal = new StringBuilder();
1918                List<IPrimitiveType<String>> childElements =
1919                                theContext.newTerser().getAllPopulatedChildElementsOfType(theResource, stringType);
1920                for (IPrimitiveType<String> nextType : childElements) {
1921                        if (stringType.equals(nextType.getClass())) {
1922                                String nextValue = nextType.getValueAsString();
1923                                if (isNotBlank(nextValue)) {
1924                                        retVal.append(nextValue.replace("\n", " ").replace("\r", " "));
1925                                        retVal.append("\n");
1926                                }
1927                        }
1928                }
1929                return retVal.toString();
1930        }
1931
1932        public static String decodeResource(byte[] theResourceBytes, ResourceEncodingEnum theResourceEncoding) {
1933                String resourceText = null;
1934                switch (theResourceEncoding) {
1935                        case JSON:
1936                                resourceText = new String(theResourceBytes, Charsets.UTF_8);
1937                                break;
1938                        case JSONC:
1939                                resourceText = GZipUtil.decompress(theResourceBytes);
1940                                break;
1941                        case DEL:
1942                        case ESR:
1943                                break;
1944                }
1945                return resourceText;
1946        }
1947
1948        private static String parseNarrativeTextIntoWords(IBaseResource theResource) {
1949
1950                StringBuilder b = new StringBuilder();
1951                if (theResource instanceof IResource) {
1952                        IResource resource = (IResource) theResource;
1953                        List<XMLEvent> xmlEvents = XmlUtil.parse(resource.getText().getDiv().getValue());
1954                        if (xmlEvents != null) {
1955                                for (XMLEvent next : xmlEvents) {
1956                                        if (next.isCharacters()) {
1957                                                Characters characters = next.asCharacters();
1958                                                b.append(characters.getData()).append(" ");
1959                                        }
1960                                }
1961                        }
1962                } else if (theResource instanceof IDomainResource) {
1963                        IDomainResource resource = (IDomainResource) theResource;
1964                        try {
1965                                String divAsString = resource.getText().getDivAsString();
1966                                List<XMLEvent> xmlEvents = XmlUtil.parse(divAsString);
1967                                if (xmlEvents != null) {
1968                                        for (XMLEvent next : xmlEvents) {
1969                                                if (next.isCharacters()) {
1970                                                        Characters characters = next.asCharacters();
1971                                                        b.append(characters.getData()).append(" ");
1972                                                }
1973                                        }
1974                                }
1975                        } catch (Exception e) {
1976                                throw new DataFormatException(Msg.code(934) + "Unable to convert DIV to string", e);
1977                        }
1978                }
1979                return b.toString();
1980        }
1981
1982        @VisibleForTesting
1983        public static void setDisableIncrementOnUpdateForUnitTest(boolean theDisableIncrementOnUpdateForUnitTest) {
1984                ourDisableIncrementOnUpdateForUnitTest = theDisableIncrementOnUpdateForUnitTest;
1985        }
1986
1987        /**
1988         * Do not call this method outside of unit tests
1989         */
1990        @VisibleForTesting
1991        public static void setValidationDisabledForUnitTest(boolean theValidationDisabledForUnitTest) {
1992                ourValidationDisabledForUnitTest = theValidationDisabledForUnitTest;
1993        }
1994
1995        private enum CreateOrUpdateByMatch {
1996                CREATE,
1997                UPDATE
1998        }
1999}