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