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