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