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