001package ca.uhn.fhir.jpa.dao;
002
003import ca.uhn.fhir.context.BaseRuntimeChildDefinition;
004import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition;
005import ca.uhn.fhir.context.BaseRuntimeElementDefinition;
006import ca.uhn.fhir.context.FhirContext;
007import ca.uhn.fhir.context.FhirVersionEnum;
008import ca.uhn.fhir.context.RuntimeChildResourceDefinition;
009import ca.uhn.fhir.context.RuntimeResourceDefinition;
010import ca.uhn.fhir.i18n.Msg;
011import ca.uhn.fhir.interceptor.api.HookParams;
012import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
013import ca.uhn.fhir.interceptor.api.Pointcut;
014import ca.uhn.fhir.interceptor.model.RequestPartitionId;
015import ca.uhn.fhir.jpa.api.config.DaoConfig;
016import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
017import ca.uhn.fhir.jpa.api.dao.IDao;
018import ca.uhn.fhir.jpa.api.dao.IJpaDao;
019import ca.uhn.fhir.jpa.api.svc.IIdHelperService;
020import ca.uhn.fhir.jpa.api.svc.ISearchCoordinatorSvc;
021import ca.uhn.fhir.jpa.dao.data.IForcedIdDao;
022import ca.uhn.fhir.jpa.dao.data.IResourceHistoryTableDao;
023import ca.uhn.fhir.jpa.dao.data.IResourceProvenanceDao;
024import ca.uhn.fhir.jpa.dao.data.IResourceTableDao;
025import ca.uhn.fhir.jpa.dao.data.IResourceTagDao;
026import ca.uhn.fhir.jpa.dao.expunge.ExpungeService;
027import ca.uhn.fhir.jpa.dao.index.DaoSearchParamSynchronizer;
028import ca.uhn.fhir.jpa.dao.index.SearchParamWithInlineReferencesExtractor;
029import ca.uhn.fhir.jpa.dao.tx.HapiTransactionService;
030import ca.uhn.fhir.jpa.delete.DeleteConflictService;
031import ca.uhn.fhir.jpa.entity.PartitionEntity;
032import ca.uhn.fhir.jpa.entity.ResourceSearchView;
033import ca.uhn.fhir.jpa.entity.Search;
034import ca.uhn.fhir.jpa.entity.SearchTypeEnum;
035import ca.uhn.fhir.jpa.model.config.PartitionSettings;
036import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
037import ca.uhn.fhir.jpa.model.entity.BaseHasResource;
038import ca.uhn.fhir.jpa.model.entity.BaseTag;
039import ca.uhn.fhir.jpa.model.entity.IBaseResourceEntity;
040import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId;
041import ca.uhn.fhir.jpa.model.entity.ResourceEncodingEnum;
042import ca.uhn.fhir.jpa.model.entity.ResourceHistoryProvenanceEntity;
043import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
044import ca.uhn.fhir.jpa.model.entity.ResourceTable;
045import ca.uhn.fhir.jpa.model.entity.ResourceTag;
046import ca.uhn.fhir.jpa.model.entity.TagDefinition;
047import ca.uhn.fhir.jpa.model.entity.TagTypeEnum;
048import ca.uhn.fhir.jpa.model.search.ExtendedLuceneIndexData;
049import ca.uhn.fhir.jpa.model.search.SearchStatusEnum;
050import ca.uhn.fhir.jpa.model.search.StorageProcessingMessage;
051import ca.uhn.fhir.jpa.model.util.JpaConstants;
052import ca.uhn.fhir.jpa.partition.IPartitionLookupSvc;
053import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc;
054import ca.uhn.fhir.jpa.search.PersistedJpaBundleProviderFactory;
055import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
056import ca.uhn.fhir.jpa.searchparam.extractor.LogicalReferenceHelper;
057import ca.uhn.fhir.jpa.searchparam.extractor.ResourceIndexedSearchParams;
058import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryMatchResult;
059import ca.uhn.fhir.jpa.searchparam.matcher.InMemoryResourceMatcher;
060import ca.uhn.fhir.jpa.sp.ISearchParamPresenceSvc;
061import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
062import ca.uhn.fhir.jpa.util.AddRemoveCount;
063import ca.uhn.fhir.jpa.util.MemoryCacheService;
064import ca.uhn.fhir.model.api.IResource;
065import ca.uhn.fhir.model.api.ResourceMetadataKeyEnum;
066import ca.uhn.fhir.model.api.Tag;
067import ca.uhn.fhir.model.api.TagList;
068import ca.uhn.fhir.model.base.composite.BaseCodingDt;
069import ca.uhn.fhir.model.primitive.IdDt;
070import ca.uhn.fhir.model.primitive.InstantDt;
071import ca.uhn.fhir.model.valueset.BundleEntryTransactionMethodEnum;
072import ca.uhn.fhir.parser.DataFormatException;
073import ca.uhn.fhir.parser.IParser;
074import ca.uhn.fhir.parser.LenientErrorHandler;
075import ca.uhn.fhir.rest.api.Constants;
076import ca.uhn.fhir.rest.api.InterceptorInvocationTimingEnum;
077import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
078import ca.uhn.fhir.rest.api.server.IBundleProvider;
079import ca.uhn.fhir.rest.api.server.RequestDetails;
080import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
081import ca.uhn.fhir.rest.api.server.storage.TransactionDetails;
082import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
083import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
084import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
085import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
086import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
087import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
088import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
089import ca.uhn.fhir.rest.server.util.ResourceSearchParams;
090import ca.uhn.fhir.util.CoverageIgnore;
091import ca.uhn.fhir.util.HapiExtensions;
092import ca.uhn.fhir.util.MetaUtil;
093import ca.uhn.fhir.util.XmlUtil;
094import com.google.common.annotations.VisibleForTesting;
095import com.google.common.base.Charsets;
096import com.google.common.collect.Sets;
097import com.google.common.hash.HashCode;
098import com.google.common.hash.HashFunction;
099import com.google.common.hash.Hashing;
100import org.apache.commons.lang3.NotImplementedException;
101import org.apache.commons.lang3.StringUtils;
102import org.apache.commons.lang3.Validate;
103import org.hl7.fhir.instance.model.api.IAnyResource;
104import org.hl7.fhir.instance.model.api.IBase;
105import org.hl7.fhir.instance.model.api.IBaseCoding;
106import org.hl7.fhir.instance.model.api.IBaseExtension;
107import org.hl7.fhir.instance.model.api.IBaseHasExtensions;
108import org.hl7.fhir.instance.model.api.IBaseMetaType;
109import org.hl7.fhir.instance.model.api.IBaseReference;
110import org.hl7.fhir.instance.model.api.IBaseResource;
111import org.hl7.fhir.instance.model.api.IDomainResource;
112import org.hl7.fhir.instance.model.api.IIdType;
113import org.hl7.fhir.instance.model.api.IPrimitiveType;
114import org.hl7.fhir.r4.model.Bundle.HTTPVerb;
115import org.jetbrains.annotations.NotNull;
116import org.slf4j.Logger;
117import org.slf4j.LoggerFactory;
118import org.springframework.beans.BeansException;
119import org.springframework.beans.factory.annotation.Autowired;
120import org.springframework.context.ApplicationContext;
121import org.springframework.context.ApplicationContextAware;
122import org.springframework.stereotype.Repository;
123import org.springframework.transaction.PlatformTransactionManager;
124import org.springframework.transaction.TransactionDefinition;
125import org.springframework.transaction.TransactionStatus;
126import org.springframework.transaction.support.TransactionCallback;
127import org.springframework.transaction.support.TransactionSynchronization;
128import org.springframework.transaction.support.TransactionSynchronizationManager;
129import org.springframework.transaction.support.TransactionTemplate;
130
131import javax.annotation.Nonnull;
132import javax.annotation.Nullable;
133import javax.annotation.PostConstruct;
134import javax.persistence.EntityManager;
135import javax.persistence.NoResultException;
136import javax.persistence.PersistenceContext;
137import javax.persistence.PersistenceContextType;
138import javax.persistence.TypedQuery;
139import javax.persistence.criteria.CriteriaBuilder;
140import javax.persistence.criteria.CriteriaQuery;
141import javax.persistence.criteria.Root;
142import javax.xml.stream.events.Characters;
143import javax.xml.stream.events.XMLEvent;
144import java.util.ArrayList;
145import java.util.Arrays;
146import java.util.Collection;
147import java.util.Collections;
148import java.util.Date;
149import java.util.HashMap;
150import java.util.HashSet;
151import java.util.IdentityHashMap;
152import java.util.List;
153import java.util.Map;
154import java.util.Set;
155import java.util.StringTokenizer;
156import java.util.UUID;
157import java.util.stream.Collectors;
158
159import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
160import static org.apache.commons.lang3.StringUtils.defaultString;
161import static org.apache.commons.lang3.StringUtils.isBlank;
162import static org.apache.commons.lang3.StringUtils.isNotBlank;
163import static org.apache.commons.lang3.StringUtils.left;
164import static org.apache.commons.lang3.StringUtils.trim;
165
166import static ca.uhn.fhir.jpa.model.util.JpaConstants.ALL_PARTITIONS_NAME;
167
168/*
169 * #%L
170 * HAPI FHIR JPA Server
171 * %%
172 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
173 * %%
174 * Licensed under the Apache License, Version 2.0 (the "License");
175 * you may not use this file except in compliance with the License.
176 * You may obtain a copy of the License at
177 *
178 * http://www.apache.org/licenses/LICENSE-2.0
179 *
180 * Unless required by applicable law or agreed to in writing, software
181 * distributed under the License is distributed on an "AS IS" BASIS,
182 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
183 * See the License for the specific language governing permissions and
184 * limitations under the License.
185 * #L%
186 */
187
188@SuppressWarnings("WeakerAccess")
189@Repository
190public abstract class BaseHapiFhirDao<T extends IBaseResource> extends BaseStorageDao implements IDao, IJpaDao<T>, ApplicationContextAware {
191
192        public static final long INDEX_STATUS_INDEXED = 1L;
193        public static final long INDEX_STATUS_INDEXING_FAILED = 2L;
194        public static final String NS_JPA_PROFILE = "https://github.com/hapifhir/hapi-fhir/ns/jpa/profile";
195        // total attempts to do a tag transaction
196        private static final int TOTAL_TAG_READ_ATTEMPTS = 10;
197        private static final Logger ourLog = LoggerFactory.getLogger(BaseHapiFhirDao.class);
198        private static boolean ourValidationDisabledForUnitTest;
199        private static boolean ourDisableIncrementOnUpdateForUnitTest = false;
200
201        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
202        protected EntityManager myEntityManager;
203        @Autowired
204        protected IIdHelperService myIdHelperService;
205        @Autowired
206        protected IForcedIdDao myForcedIdDao;
207        @Autowired
208        protected IResourceProvenanceDao myResourceProvenanceDao;
209        @Autowired
210        protected ISearchCoordinatorSvc mySearchCoordinatorSvc;
211        @Autowired
212        protected ITermReadSvc myTerminologySvc;
213        @Autowired
214        protected IResourceHistoryTableDao myResourceHistoryTableDao;
215        @Autowired
216        protected IResourceTableDao myResourceTableDao;
217        @Autowired
218        protected IResourceTagDao myResourceTagDao;
219        @Autowired
220        protected DeleteConflictService myDeleteConflictService;
221        @Autowired
222        protected IInterceptorBroadcaster myInterceptorBroadcaster;
223        @Autowired
224        protected DaoRegistry myDaoRegistry;
225        @Autowired
226        protected InMemoryResourceMatcher myInMemoryResourceMatcher;
227        @Autowired
228        ExpungeService myExpungeService;
229        @Autowired
230        private HistoryBuilderFactory myHistoryBuilderFactory;
231        @Autowired
232        private DaoConfig myConfig;
233        @Autowired
234        private PlatformTransactionManager myPlatformTransactionManager;
235        @Autowired
236        private ISearchCacheSvc mySearchCacheSvc;
237        @Autowired
238        private ISearchParamPresenceSvc mySearchParamPresenceSvc;
239        @Autowired
240        private SearchParamWithInlineReferencesExtractor mySearchParamWithInlineReferencesExtractor;
241        @Autowired
242        private DaoSearchParamSynchronizer myDaoSearchParamSynchronizer;
243        @Autowired
244        private SearchBuilderFactory mySearchBuilderFactory;
245        private FhirContext myContext;
246        private ApplicationContext myApplicationContext;
247        @Autowired
248        private PartitionSettings myPartitionSettings;
249        @Autowired
250        private RequestPartitionHelperSvc myRequestPartitionHelperSvc;
251        @Autowired
252        private PersistedJpaBundleProviderFactory myPersistedJpaBundleProviderFactory;
253        @Autowired
254        private IPartitionLookupSvc myPartitionLookupSvc;
255        @Autowired
256        private MemoryCacheService myMemoryCacheService;
257        @Autowired(required = false)
258        private IFulltextSearchSvc myFulltextSearchSvc;
259
260        @Autowired
261        private PlatformTransactionManager myTransactionManager;
262
263        @VisibleForTesting
264        public void setSearchParamPresenceSvc(ISearchParamPresenceSvc theSearchParamPresenceSvc) {
265                mySearchParamPresenceSvc = theSearchParamPresenceSvc;
266        }
267
268        @Override
269        protected IInterceptorBroadcaster getInterceptorBroadcaster() {
270                return myInterceptorBroadcaster;
271        }
272
273        protected ApplicationContext getApplicationContext() {
274                return myApplicationContext;
275        }
276
277        @Override
278        public void setApplicationContext(ApplicationContext theApplicationContext) throws BeansException {
279                /*
280                 * We do a null check here because Smile's module system tries to
281                 * initialize the application context twice if two modules depend on
282                 * the persistence module. The second time sets the dependency's appctx.
283                 */
284                if (myApplicationContext == null) {
285                        myApplicationContext = theApplicationContext;
286                }
287        }
288
289        private void extractTagsHapi(TransactionDetails theTransactionDetails, IResource theResource, ResourceTable theEntity, Set<ResourceTag> allDefs) {
290                TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(theResource);
291                if (tagList != null) {
292                        for (Tag next : tagList) {
293                                TagDefinition def = getTagOrNull(theTransactionDetails, TagTypeEnum.TAG, next.getScheme(), next.getTerm(), next.getLabel());
294                                if (def != null) {
295                                        ResourceTag tag = theEntity.addTag(def);
296                                        allDefs.add(tag);
297                                        theEntity.setHasTags(true);
298                                }
299                        }
300                }
301
302                List<BaseCodingDt> securityLabels = ResourceMetadataKeyEnum.SECURITY_LABELS.get(theResource);
303                if (securityLabels != null) {
304                        for (BaseCodingDt next : securityLabels) {
305                                TagDefinition def = getTagOrNull(theTransactionDetails, TagTypeEnum.SECURITY_LABEL, next.getSystemElement().getValue(), next.getCodeElement().getValue(), next.getDisplayElement().getValue());
306                                if (def != null) {
307                                        ResourceTag tag = theEntity.addTag(def);
308                                        allDefs.add(tag);
309                                        theEntity.setHasTags(true);
310                                }
311                        }
312                }
313
314                List<IdDt> profiles = ResourceMetadataKeyEnum.PROFILES.get(theResource);
315                if (profiles != null) {
316                        for (IIdType next : profiles) {
317                                TagDefinition def = getTagOrNull(theTransactionDetails, TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null);
318                                if (def != null) {
319                                        ResourceTag tag = theEntity.addTag(def);
320                                        allDefs.add(tag);
321                                        theEntity.setHasTags(true);
322                                }
323                        }
324                }
325        }
326
327        private void extractTagsRi(TransactionDetails theTransactionDetails, IAnyResource theResource, ResourceTable theEntity, Set<ResourceTag> theAllTags) {
328                List<? extends IBaseCoding> tagList = theResource.getMeta().getTag();
329                if (tagList != null) {
330                        for (IBaseCoding next : tagList) {
331                                TagDefinition def = getTagOrNull(theTransactionDetails, TagTypeEnum.TAG, next.getSystem(), next.getCode(), next.getDisplay());
332                                if (def != null) {
333                                        ResourceTag tag = theEntity.addTag(def);
334                                        theAllTags.add(tag);
335                                        theEntity.setHasTags(true);
336                                }
337                        }
338                }
339
340                List<? extends IBaseCoding> securityLabels = theResource.getMeta().getSecurity();
341                if (securityLabels != null) {
342                        for (IBaseCoding next : securityLabels) {
343                                TagDefinition def = getTagOrNull(theTransactionDetails, TagTypeEnum.SECURITY_LABEL, next.getSystem(), next.getCode(), next.getDisplay());
344                                if (def != null) {
345                                        ResourceTag tag = theEntity.addTag(def);
346                                        theAllTags.add(tag);
347                                        theEntity.setHasTags(true);
348                                }
349                        }
350                }
351
352                List<? extends IPrimitiveType<String>> profiles = theResource.getMeta().getProfile();
353                if (profiles != null) {
354                        for (IPrimitiveType<String> next : profiles) {
355                                TagDefinition def = getTagOrNull(theTransactionDetails, TagTypeEnum.PROFILE, NS_JPA_PROFILE, next.getValue(), null);
356                                if (def != null) {
357                                        ResourceTag tag = theEntity.addTag(def);
358                                        theAllTags.add(tag);
359                                        theEntity.setHasTags(true);
360                                }
361                        }
362                }
363
364        }
365
366        private Set<ResourceTag> getAllTagDefinitions(ResourceTable theEntity) {
367                HashSet<ResourceTag> retVal = Sets.newHashSet();
368                if (theEntity.isHasTags()) {
369                        for (ResourceTag next : theEntity.getTags()) {
370                                retVal.add(next);
371                        }
372                }
373                return retVal;
374        }
375
376        @Override
377        public DaoConfig getConfig() {
378                return myConfig;
379        }
380
381        @Override
382        public FhirContext getContext() {
383                return myContext;
384        }
385
386        @Autowired
387        public void setContext(FhirContext theContext) {
388                super.myFhirContext = theContext;
389                myContext = theContext;
390        }
391
392        public FhirContext getContext(FhirVersionEnum theVersion) {
393                Validate.notNull(theVersion, "theVersion must not be null");
394                if (theVersion == myFhirContext.getVersion().getVersion()) {
395                        return myFhirContext;
396                }
397                return FhirContext.forCached(theVersion);
398        }
399
400        /**
401         * <code>null</code> will only be returned if the scheme and tag are both blank
402         */
403        protected TagDefinition getTagOrNull(TransactionDetails theTransactionDetails, TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
404                if (isBlank(theScheme) && isBlank(theTerm) && isBlank(theLabel)) {
405                        return null;
406                }
407
408                MemoryCacheService.TagDefinitionCacheKey key = toTagDefinitionMemoryCacheKey(theTagType, theScheme, theTerm);
409
410                TagDefinition retVal = myMemoryCacheService.getIfPresent(MemoryCacheService.CacheEnum.TAG_DEFINITION, key);
411
412                if (retVal == null) {
413                        HashMap<MemoryCacheService.TagDefinitionCacheKey, TagDefinition> resolvedTagDefinitions = theTransactionDetails.getOrCreateUserData(HapiTransactionService.XACT_USERDATA_KEY_RESOLVED_TAG_DEFINITIONS, () -> new HashMap<>());
414                        retVal = resolvedTagDefinitions.get(key);
415
416                        if (retVal == null) {
417                                // actual DB hit(s) happen here
418                                retVal = getOrCreateTag(theTagType, theScheme, theTerm, theLabel);
419
420                                TransactionSynchronization sync = new AddTagDefinitionToCacheAfterCommitSynchronization(key, retVal);
421                                TransactionSynchronizationManager.registerSynchronization(sync);
422
423                                resolvedTagDefinitions.put(key, retVal);
424                        }
425                }
426
427                return retVal;
428        }
429
430        /**
431         * Gets the tag defined by the fed in values, or saves it if it does not
432         * exist.
433         * <p>
434         * Can also throw an InternalErrorException if something bad happens.
435         */
436        private TagDefinition getOrCreateTag(TagTypeEnum theTagType, String theScheme, String theTerm, String theLabel) {
437                CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
438                CriteriaQuery<TagDefinition> cq = builder.createQuery(TagDefinition.class);
439                Root<TagDefinition> from = cq.from(TagDefinition.class);
440
441                if (isNotBlank(theScheme)) {
442                        cq.where(
443                                builder.and(
444                                        builder.equal(from.get("myTagType"), theTagType),
445                                        builder.equal(from.get("mySystem"), theScheme),
446                                        builder.equal(from.get("myCode"), theTerm)));
447                } else {
448                        cq.where(
449                                builder.and(
450                                        builder.equal(from.get("myTagType"), theTagType),
451                                        builder.isNull(from.get("mySystem")),
452                                        builder.equal(from.get("myCode"), theTerm)));
453                }
454
455                TypedQuery<TagDefinition> q = myEntityManager.createQuery(cq);
456
457                TransactionTemplate template = new TransactionTemplate(myTransactionManager);
458                template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
459
460                // this transaction will attempt to get or create the tag,
461                // repeating (on any failure) 10 times.
462                // if it fails more than this, we will throw exceptions
463                TagDefinition retVal;
464                int count = 0;
465                HashSet<Throwable> throwables = new HashSet<>();
466                do {
467                        try {
468                                retVal = template.execute(new TransactionCallback<TagDefinition>() {
469
470                                        // do the actual DB call(s) to read and/or write the values
471                                        private TagDefinition readOrCreate() {
472                                                TagDefinition val;
473                                                try {
474                                                        val = q.getSingleResult();
475                                                } catch (NoResultException e) {
476                                                        val = new TagDefinition(theTagType, theScheme, theTerm, theLabel);
477                                                        myEntityManager.persist(val);
478                                                }
479                                                return val;
480                                        }
481
482                                        @Override
483                                        public TagDefinition doInTransaction(TransactionStatus status) {
484                                                TagDefinition tag = null;
485
486                                                try {
487                                                        tag = readOrCreate();
488                                                } catch (Exception ex) {
489                                                        // log any exceptions - just in case
490                                                        // they may be signs of things to come...
491                                                        ourLog.warn(
492                                                                "Tag read/write failed: "
493                                                                        + ex.getMessage() + ". "
494                                                                        + "This is not a failure on its own, "
495                                                                        + "but could be useful information in the result of an actual failure."
496                                                        );
497                                                        throwables.add(ex);
498                                                }
499
500                                                return tag;
501                                        }
502                                });
503                        } catch (Exception ex) {
504                                // transaction template can fail if connections to db are exhausted
505                                // and/or timeout
506                                ourLog.warn("Transaction failed with: "
507                                        + ex.getMessage() + ". "
508                                        + "Transaction will rollback and be reattempted."
509                                );
510                                retVal = null;
511                        }
512                        count++;
513                } while (retVal == null && count < TOTAL_TAG_READ_ATTEMPTS);
514
515                if (retVal == null) {
516                        // if tag is still null,
517                        // something bad must be happening
518                        // - throw
519                        String msg = throwables.stream()
520                                .map(Throwable::getMessage)
521                                .collect(Collectors.joining(", "));
522                        throw new InternalErrorException(
523                                Msg.code(2023)
524                                        + "Tag get/create failed after "
525                                        + TOTAL_TAG_READ_ATTEMPTS
526                                        + " attempts with error(s): "
527                                        + msg
528                        );
529                }
530
531                return retVal;
532        }
533
534        protected IBundleProvider history(RequestDetails theRequest, String theResourceType, Long theResourcePid, Date theRangeStartInclusive, Date theRangeEndInclusive, Integer theOffset) {
535
536                String resourceName = defaultIfBlank(theResourceType, null);
537
538                Search search = new Search();
539                search.setOffset(theOffset);
540                search.setDeleted(false);
541                search.setCreated(new Date());
542                search.setLastUpdated(theRangeStartInclusive, theRangeEndInclusive);
543                search.setUuid(UUID.randomUUID().toString());
544                search.setResourceType(resourceName);
545                search.setResourceId(theResourcePid);
546                search.setSearchType(SearchTypeEnum.HISTORY);
547                search.setStatus(SearchStatusEnum.FINISHED);
548
549                return myPersistedJpaBundleProviderFactory.newInstance(theRequest, search);
550        }
551
552        void incrementId(T theResource, ResourceTable theSavedEntity, IIdType theResourceId) {
553                String newVersion;
554                long newVersionLong;
555                if (theResourceId == null || theResourceId.getVersionIdPart() == null) {
556                        newVersion = "1";
557                        newVersionLong = 1;
558                } else {
559                        newVersionLong = theResourceId.getVersionIdPartAsLong() + 1;
560                        newVersion = Long.toString(newVersionLong);
561                }
562
563                assert theResourceId != null;
564                IIdType newId = theResourceId.withVersion(newVersion);
565                theResource.getIdElement().setValue(newId.getValue());
566                theSavedEntity.setVersion(newVersionLong);
567        }
568
569        public boolean isLogicalReference(IIdType theId) {
570                return LogicalReferenceHelper.isLogicalReference(myConfig.getModelConfig(), theId);
571        }
572
573        /**
574         * Returns true if the resource has changed (either the contents or the tags)
575         */
576        protected EncodedResource populateResourceIntoEntity(TransactionDetails theTransactionDetails, RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity, boolean thePerformIndexing) {
577                if (theEntity.getResourceType() == null) {
578                        theEntity.setResourceType(toResourceName(theResource));
579                }
580
581                byte[] resourceBinary;
582                String resourceText;
583                ResourceEncodingEnum encoding;
584                boolean changed = false;
585
586                if (theEntity.getDeleted() == null) {
587
588                        if (thePerformIndexing) {
589
590                                encoding = myConfig.getResourceEncoding();
591
592                                String resourceType = theEntity.getResourceType();
593
594                                List<String> excludeElements = new ArrayList<>(8);
595                                excludeElements.add("id");
596
597                                IBaseMetaType meta = theResource.getMeta();
598                                boolean hasExtensions = false;
599                                IBaseExtension<?, ?> sourceExtension = null;
600                                if (meta instanceof IBaseHasExtensions) {
601                                        List<? extends IBaseExtension<?, ?>> extensions = ((IBaseHasExtensions) meta).getExtension();
602                                        if (!extensions.isEmpty()) {
603                                                hasExtensions = true;
604
605                                                /*
606                                                 * FHIR DSTU3 did not have the Resource.meta.source field, so we use a
607                                                 * custom HAPI FHIR extension in Resource.meta to store that field. However,
608                                                 * we put the value for that field in a separate table so we don't want to serialize
609                                                 * it into the stored BLOB. Therefore: remove it from the resource temporarily
610                                                 * and restore it afterward.
611                                                 */
612                                                if (myFhirContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) {
613                                                        for (int i = 0; i < extensions.size(); i++) {
614                                                                if (extensions.get(i).getUrl().equals(HapiExtensions.EXT_META_SOURCE)) {
615                                                                        sourceExtension = extensions.remove(i);
616                                                                        i--;
617                                                                }
618                                                        }
619                                                }
620
621                                        }
622                                }
623
624                                boolean inlineTagMode = getConfig().getTagStorageMode() == DaoConfig.TagStorageModeEnum.INLINE;
625                                if (hasExtensions || inlineTagMode) {
626                                        if (!inlineTagMode) {
627                                                excludeElements.add(resourceType + ".meta.profile");
628                                                excludeElements.add(resourceType + ".meta.tag");
629                                                excludeElements.add(resourceType + ".meta.security");
630                                        }
631                                        excludeElements.add(resourceType + ".meta.versionId");
632                                        excludeElements.add(resourceType + ".meta.lastUpdated");
633                                        excludeElements.add(resourceType + ".meta.source");
634                                } else {
635                                        /*
636                                         * If there are no extensions in the meta element, we can just exclude the
637                                         * whole meta element, which avoids adding an empty "meta":{}
638                                         * from showing up in the serialized JSON.
639                                         */
640                                        excludeElements.add(resourceType + ".meta");
641                                }
642
643                                theEntity.setFhirVersion(myContext.getVersion().getVersion());
644
645                                HashFunction sha256 = Hashing.sha256();
646                                HashCode hashCode;
647                                String encodedResource = encodeResource(theResource, encoding, excludeElements, myContext);
648                                if (getConfig().getInlineResourceTextBelowSize() > 0 && encodedResource.length() < getConfig().getInlineResourceTextBelowSize()) {
649                                        resourceText = encodedResource;
650                                        resourceBinary = null;
651                                        encoding = ResourceEncodingEnum.JSON;
652                                        hashCode = sha256.hashUnencodedChars(encodedResource);
653                                } else {
654                                        resourceText = null;
655                                        switch (encoding) {
656                                                case JSON:
657                                                        resourceBinary = encodedResource.getBytes(Charsets.UTF_8);
658                                                        break;
659                                                case JSONC:
660                                                        resourceBinary = GZipUtil.compress(encodedResource);
661                                                        break;
662                                                default:
663                                                case DEL:
664                                                        resourceBinary = new byte[0];
665                                                        break;
666                                        }
667                                        hashCode = sha256.hashBytes(resourceBinary);
668                                }
669
670                                String hashSha256 = hashCode.toString();
671                                if (hashSha256.equals(theEntity.getHashSha256()) == false) {
672                                        changed = true;
673                                }
674                                theEntity.setHashSha256(hashSha256);
675
676
677                                if (sourceExtension != null) {
678                                        IBaseExtension<?, ?> newSourceExtension = ((IBaseHasExtensions) meta).addExtension();
679                                        newSourceExtension.setUrl(sourceExtension.getUrl());
680                                        newSourceExtension.setValue(sourceExtension.getValue());
681                                }
682
683                        } else {
684
685                                encoding = null;
686                                resourceBinary = null;
687                                resourceText = null;
688
689                        }
690
691                        boolean skipUpdatingTags = myConfig.isMassIngestionMode() && theEntity.isHasTags();
692                        skipUpdatingTags |= myConfig.getTagStorageMode() == DaoConfig.TagStorageModeEnum.INLINE;
693
694                        if (!skipUpdatingTags) {
695                                changed |= updateTags(theTransactionDetails, theRequest, theResource, theEntity);
696                        }
697
698                } else {
699
700                        theEntity.setHashSha256(null);
701                        resourceBinary = null;
702                        resourceText = null;
703                        encoding = ResourceEncodingEnum.DEL;
704
705                }
706
707                if (thePerformIndexing && !changed) {
708                        if (theEntity.getId() == null) {
709                                changed = true;
710                        } else if (myConfig.isMassIngestionMode()) {
711
712                                // Don't check existing - We'll rely on the SHA256 hash only
713
714                        } else if (theEntity.getVersion() == 1L && theEntity.getCurrentVersionEntity() == null) {
715
716                                // No previous version if this is the first version
717
718                        } else {
719                                ResourceHistoryTable currentHistoryVersion = theEntity.getCurrentVersionEntity();
720                                if (currentHistoryVersion == null) {
721                                        currentHistoryVersion = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(theEntity.getId(), theEntity.getVersion());
722                                }
723                                if (currentHistoryVersion == null || !currentHistoryVersion.hasResource()) {
724                                        changed = true;
725                                } else {
726                                        changed = !Arrays.equals(currentHistoryVersion.getResource(), resourceBinary);
727                                }
728                        }
729                }
730
731                EncodedResource retVal = new EncodedResource();
732                retVal.setEncoding(encoding);
733                retVal.setResourceBinary(resourceBinary);
734                retVal.setResourceText(resourceText);
735                retVal.setChanged(changed);
736
737                return retVal;
738        }
739
740        private boolean updateTags(TransactionDetails theTransactionDetails, RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity) {
741                Set<ResourceTag> allDefs = new HashSet<>();
742                Set<ResourceTag> allTagsOld = getAllTagDefinitions(theEntity);
743
744                if (theResource instanceof IResource) {
745                        extractTagsHapi(theTransactionDetails, (IResource) theResource, theEntity, allDefs);
746                } else {
747                        extractTagsRi(theTransactionDetails, (IAnyResource) theResource, theEntity, allDefs);
748                }
749
750                RuntimeResourceDefinition def = myContext.getResourceDefinition(theResource);
751                if (def.isStandardType() == false) {
752                        String profile = def.getResourceProfile("");
753                        if (isNotBlank(profile)) {
754                                TagDefinition profileDef = getTagOrNull(theTransactionDetails, TagTypeEnum.PROFILE, NS_JPA_PROFILE, profile, null);
755
756                                ResourceTag tag = theEntity.addTag(profileDef);
757                                allDefs.add(tag);
758                                theEntity.setHasTags(true);
759                        }
760                }
761
762                Set<ResourceTag> allTagsNew = getAllTagDefinitions(theEntity);
763                Set<TagDefinition> allDefsPresent = new HashSet<>();
764                allTagsNew.forEach(tag -> {
765
766                        // Don't keep duplicate tags
767                        if (!allDefsPresent.add(tag.getTag())) {
768                                theEntity.getTags().remove(tag);
769                        }
770
771                        // Drop any tags that have been removed
772                        if (!allDefs.contains(tag)) {
773                                if (shouldDroppedTagBeRemovedOnUpdate(theRequest, tag)) {
774                                        theEntity.getTags().remove(tag);
775                                }
776                        }
777
778                });
779
780                theEntity.setHasTags(!allTagsNew.isEmpty());
781                return !allTagsOld.equals(allTagsNew);
782        }
783
784        @SuppressWarnings("unchecked")
785        private <R extends IBaseResource> R populateResourceMetadataHapi(Class<R> theResourceType, IBaseResourceEntity theEntity, @Nullable Collection<? extends BaseTag> theTagList, boolean theForHistoryOperation, IResource res, Long theVersion) {
786                R retVal = (R) res;
787                if (theEntity.getDeleted() != null) {
788                        res = (IResource) myContext.getResourceDefinition(theResourceType).newInstance();
789                        retVal = (R) res;
790                        ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted()));
791                        if (theForHistoryOperation) {
792                                ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.DELETE);
793                        }
794                } else if (theForHistoryOperation) {
795                        /*
796                         * If the create and update times match, this was when the resource was created so we should mark it as a POST. Otherwise, it's a PUT.
797                         */
798                        Date published = theEntity.getPublished().getValue();
799                        Date updated = theEntity.getUpdated().getValue();
800                        if (published.equals(updated)) {
801                                ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.POST);
802                        } else {
803                                ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, BundleEntryTransactionMethodEnum.PUT);
804                        }
805                }
806
807                res.setId(theEntity.getIdDt().withVersion(theVersion.toString()));
808
809                ResourceMetadataKeyEnum.VERSION.put(res, Long.toString(theEntity.getVersion()));
810                ResourceMetadataKeyEnum.PUBLISHED.put(res, theEntity.getPublished());
811                ResourceMetadataKeyEnum.UPDATED.put(res, theEntity.getUpdated());
812                IDao.RESOURCE_PID.put(res, theEntity.getResourceId());
813
814                if (theTagList != null) {
815                        if (theEntity.isHasTags()) {
816                                TagList tagList = new TagList();
817                                List<IBaseCoding> securityLabels = new ArrayList<>();
818                                List<IdDt> profiles = new ArrayList<>();
819                                for (BaseTag next : theTagList) {
820                                        switch (next.getTag().getTagType()) {
821                                                case PROFILE:
822                                                        profiles.add(new IdDt(next.getTag().getCode()));
823                                                        break;
824                                                case SECURITY_LABEL:
825                                                        IBaseCoding secLabel = (IBaseCoding) myContext.getVersion().newCodingDt();
826                                                        secLabel.setSystem(next.getTag().getSystem());
827                                                        secLabel.setCode(next.getTag().getCode());
828                                                        secLabel.setDisplay(next.getTag().getDisplay());
829                                                        securityLabels.add(secLabel);
830                                                        break;
831                                                case TAG:
832                                                        tagList.add(new Tag(next.getTag().getSystem(), next.getTag().getCode(), next.getTag().getDisplay()));
833                                                        break;
834                                        }
835                                }
836                                if (tagList.size() > 0) {
837                                        ResourceMetadataKeyEnum.TAG_LIST.put(res, tagList);
838                                }
839                                if (securityLabels.size() > 0) {
840                                        ResourceMetadataKeyEnum.SECURITY_LABELS.put(res, toBaseCodingList(securityLabels));
841                                }
842                                if (profiles.size() > 0) {
843                                        ResourceMetadataKeyEnum.PROFILES.put(res, profiles);
844                                }
845                        }
846                }
847
848                return retVal;
849        }
850
851        @SuppressWarnings("unchecked")
852        private <R extends IBaseResource> R populateResourceMetadataRi(Class<R> theResourceType, IBaseResourceEntity theEntity, @Nullable Collection<? extends BaseTag> theTagList, boolean theForHistoryOperation, IAnyResource res, Long theVersion) {
853                R retVal = (R) res;
854                if (theEntity.getDeleted() != null) {
855                        res = (IAnyResource) myContext.getResourceDefinition(theResourceType).newInstance();
856                        retVal = (R) res;
857                        ResourceMetadataKeyEnum.DELETED_AT.put(res, new InstantDt(theEntity.getDeleted()));
858                        if (theForHistoryOperation) {
859                                ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.DELETE.toCode());
860                        }
861                } else if (theForHistoryOperation) {
862                        /*
863                         * If the create and update times match, this was when the resource was created so we should mark it as a POST. Otherwise, it's a PUT.
864                         */
865                        Date published = theEntity.getPublished().getValue();
866                        Date updated = theEntity.getUpdated().getValue();
867                        if (published.equals(updated)) {
868                                ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.POST.toCode());
869                        } else {
870                                ResourceMetadataKeyEnum.ENTRY_TRANSACTION_METHOD.put(res, HTTPVerb.PUT.toCode());
871                        }
872                }
873
874                res.getMeta().setLastUpdated(null);
875                res.getMeta().setVersionId(null);
876
877                updateResourceMetadata(theEntity, res);
878                res.setId(res.getIdElement().withVersion(theVersion.toString()));
879
880                res.getMeta().setLastUpdated(theEntity.getUpdatedDate());
881                IDao.RESOURCE_PID.put(res, theEntity.getResourceId());
882
883                if (theTagList != null) {
884                        res.getMeta().getTag().clear();
885                        res.getMeta().getProfile().clear();
886                        res.getMeta().getSecurity().clear();
887                        for (BaseTag next : theTagList) {
888                                switch (next.getTag().getTagType()) {
889                                        case PROFILE:
890                                                res.getMeta().addProfile(next.getTag().getCode());
891                                                break;
892                                        case SECURITY_LABEL:
893                                                IBaseCoding sec = res.getMeta().addSecurity();
894                                                sec.setSystem(next.getTag().getSystem());
895                                                sec.setCode(next.getTag().getCode());
896                                                sec.setDisplay(next.getTag().getDisplay());
897                                                break;
898                                        case TAG:
899                                                IBaseCoding tag = res.getMeta().addTag();
900                                                tag.setSystem(next.getTag().getSystem());
901                                                tag.setCode(next.getTag().getCode());
902                                                tag.setDisplay(next.getTag().getDisplay());
903                                                break;
904                                }
905                        }
906                }
907
908                return retVal;
909        }
910
911        /**
912         * Subclasses may override to provide behaviour. Called when a pre-existing resource has been updated in the database
913         *
914         * @param theEntity The resource
915         */
916        protected void postDelete(ResourceTable theEntity) {
917                // nothing
918        }
919
920        /**
921         * Subclasses may override to provide behaviour. Called when a resource has been inserted into the database for the first time.
922         *
923         * @param theEntity   The entity being updated (Do not modify the entity! Undefined behaviour will occur!)
924         * @param theResource The resource being persisted
925         */
926        protected void postPersist(ResourceTable theEntity, T theResource) {
927                // nothing
928        }
929
930        /**
931         * Subclasses may override to provide behaviour. Called when a pre-existing resource has been updated in the database
932         *
933         * @param theEntity   The resource
934         * @param theResource The resource being persisted
935         */
936        protected void postUpdate(ResourceTable theEntity, T theResource) {
937                // nothing
938        }
939
940        @CoverageIgnore
941        public BaseHasResource readEntity(IIdType theValueId, RequestDetails theRequest) {
942                throw new NotImplementedException(Msg.code(927) + "");
943        }
944
945        /**
946         * 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.
947         * <p>
948         * The default implementation removes any profile declarations, but leaves tags and security labels in place. Subclasses may choose to override and change this behaviour.
949         * </p>
950         * <p>
951         * 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 folows.
952         * </p>
953         *
954         * @param theTag The tag
955         * @return Returns <code>true</code> if the tag should be removed
956         */
957        protected boolean shouldDroppedTagBeRemovedOnUpdate(RequestDetails theRequest, ResourceTag theTag) {
958
959                Set<TagTypeEnum> metaSnapshotModeTokens = null;
960
961                if (theRequest != null) {
962                        List<String> metaSnapshotMode = theRequest.getHeaders(JpaConstants.HEADER_META_SNAPSHOT_MODE);
963                        if (metaSnapshotMode != null && !metaSnapshotMode.isEmpty()) {
964                                metaSnapshotModeTokens = new HashSet<>();
965                                for (String nextHeaderValue : metaSnapshotMode) {
966                                        StringTokenizer tok = new StringTokenizer(nextHeaderValue, ",");
967                                        while (tok.hasMoreTokens()) {
968                                                switch (trim(tok.nextToken())) {
969                                                        case "TAG":
970                                                                metaSnapshotModeTokens.add(TagTypeEnum.TAG);
971                                                                break;
972                                                        case "PROFILE":
973                                                                metaSnapshotModeTokens.add(TagTypeEnum.PROFILE);
974                                                                break;
975                                                        case "SECURITY_LABEL":
976                                                                metaSnapshotModeTokens.add(TagTypeEnum.SECURITY_LABEL);
977                                                                break;
978                                                }
979                                        }
980                                }
981                        }
982                }
983
984                if (metaSnapshotModeTokens == null) {
985                        metaSnapshotModeTokens = Collections.singleton(TagTypeEnum.PROFILE);
986                }
987
988                return metaSnapshotModeTokens.contains(theTag.getTag().getTagType());
989        }
990
991        @Override
992        public IBaseResource toResource(BaseHasResource theEntity, boolean theForHistoryOperation) {
993                RuntimeResourceDefinition type = myContext.getResourceDefinition(theEntity.getResourceType());
994                Class<? extends IBaseResource> resourceType = type.getImplementingClass();
995                return toResource(resourceType, theEntity, null, theForHistoryOperation);
996        }
997
998        @SuppressWarnings("unchecked")
999        @Override
1000        public <R extends IBaseResource> R toResource(Class<R> theResourceType, IBaseResourceEntity theEntity, Collection<ResourceTag> theTagList, boolean theForHistoryOperation) {
1001
1002                // 1. get resource, it's encoding and the tags if any
1003                byte[] resourceBytes;
1004                String resourceText;
1005                ResourceEncodingEnum resourceEncoding;
1006                @Nullable
1007                Collection<? extends BaseTag> tagList = Collections.emptyList();
1008                long version;
1009                String provenanceSourceUri = null;
1010                String provenanceRequestId = null;
1011
1012                if (theEntity instanceof ResourceHistoryTable) {
1013                        ResourceHistoryTable history = (ResourceHistoryTable) theEntity;
1014                        resourceBytes = history.getResource();
1015                        resourceText = history.getResourceTextVc();
1016                        resourceEncoding = history.getEncoding();
1017                        switch (getConfig().getTagStorageMode()) {
1018                                case VERSIONED:
1019                                default:
1020                                        if (history.isHasTags()) {
1021                                                tagList = history.getTags();
1022                                        }
1023                                        break;
1024                                case NON_VERSIONED:
1025                                        if (history.getResourceTable().isHasTags()) {
1026                                                tagList = history.getResourceTable().getTags();
1027                                        }
1028                                        break;
1029                                case INLINE:
1030                                        tagList = null;
1031                        }
1032                        version = history.getVersion();
1033                        if (history.getProvenance() != null) {
1034                                provenanceRequestId = history.getProvenance().getRequestId();
1035                                provenanceSourceUri = history.getProvenance().getSourceUri();
1036                        }
1037                } else if (theEntity instanceof ResourceTable) {
1038                        ResourceTable resource = (ResourceTable) theEntity;
1039                        ResourceHistoryTable history;
1040                        if (resource.getCurrentVersionEntity() != null) {
1041                                history = resource.getCurrentVersionEntity();
1042                        } else {
1043                                version = theEntity.getVersion();
1044                                history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(theEntity.getId(), version);
1045                                ((ResourceTable) theEntity).setCurrentVersionEntity(history);
1046
1047                                while (history == null) {
1048                                        if (version > 1L) {
1049                                                version--;
1050                                                history = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(theEntity.getId(), version);
1051                                        } else {
1052                                                return null;
1053                                        }
1054                                }
1055                        }
1056
1057                        resourceBytes = history.getResource();
1058                        resourceEncoding = history.getEncoding();
1059                        resourceText = history.getResourceTextVc();
1060                        switch (getConfig().getTagStorageMode()) {
1061                                case VERSIONED:
1062                                case NON_VERSIONED:
1063                                        if (resource.isHasTags()) {
1064                                                tagList = resource.getTags();
1065                                        } else {
1066                                                tagList = Collections.emptyList();
1067                                        }
1068                                        break;
1069                                case INLINE:
1070                                        tagList = null;
1071                                        break;
1072                        }
1073                        version = history.getVersion();
1074                        if (history.getProvenance() != null) {
1075                                provenanceRequestId = history.getProvenance().getRequestId();
1076                                provenanceSourceUri = history.getProvenance().getSourceUri();
1077                        }
1078                } else if (theEntity instanceof ResourceSearchView) {
1079                        // This is the search View
1080                        ResourceSearchView view = (ResourceSearchView) theEntity;
1081                        resourceBytes = view.getResource();
1082                        resourceText = view.getResourceTextVc();
1083                        resourceEncoding = view.getEncoding();
1084                        version = view.getVersion();
1085                        provenanceRequestId = view.getProvenanceRequestId();
1086                        provenanceSourceUri = view.getProvenanceSourceUri();
1087                        switch (getConfig().getTagStorageMode()) {
1088                                case VERSIONED:
1089                                case NON_VERSIONED:
1090                                        if (theTagList != null) {
1091                                                tagList = theTagList;
1092                                        } else {
1093                                                tagList = Collections.emptyList();
1094                                        }
1095                                        break;
1096                                case INLINE:
1097                                        tagList = null;
1098                                        break;
1099                        }
1100                } else {
1101                        // something wrong
1102                        return null;
1103                }
1104
1105                // 2. get The text
1106                String decodedResourceText;
1107                if (resourceText != null) {
1108                        decodedResourceText = resourceText;
1109                } else {
1110                        decodedResourceText = decodeResource(resourceBytes, resourceEncoding);
1111                }
1112
1113                // 3. Use the appropriate custom type if one is specified in the context
1114                Class<R> resourceType = theResourceType;
1115                if (tagList != null) {
1116                        if (myContext.hasDefaultTypeForProfile()) {
1117                                for (BaseTag nextTag : tagList) {
1118                                        if (nextTag.getTag().getTagType() == TagTypeEnum.PROFILE) {
1119                                                String profile = nextTag.getTag().getCode();
1120                                                if (isNotBlank(profile)) {
1121                                                        Class<? extends IBaseResource> newType = myContext.getDefaultTypeForProfile(profile);
1122                                                        if (newType != null && theResourceType.isAssignableFrom(newType)) {
1123                                                                ourLog.debug("Using custom type {} for profile: {}", newType.getName(), profile);
1124                                                                resourceType = (Class<R>) newType;
1125                                                                break;
1126                                                        }
1127                                                }
1128                                        }
1129                                }
1130                        }
1131                }
1132
1133                // 4. parse the text to FHIR
1134                R retVal;
1135                if (resourceEncoding != ResourceEncodingEnum.DEL) {
1136
1137                        LenientErrorHandler errorHandler = new LenientErrorHandler(false).setErrorOnInvalidValue(false);
1138                        IParser parser = new TolerantJsonParser(getContext(theEntity.getFhirVersion()), errorHandler, theEntity.getId());
1139
1140                        try {
1141                                retVal = parser.parseResource(resourceType, decodedResourceText);
1142                        } catch (Exception e) {
1143                                StringBuilder b = new StringBuilder();
1144                                b.append("Failed to parse database resource[");
1145                                b.append(myFhirContext.getResourceType(resourceType));
1146                                b.append("/");
1147                                b.append(theEntity.getIdDt().getIdPart());
1148                                b.append(" (pid ");
1149                                b.append(theEntity.getId());
1150                                b.append(", version ");
1151                                b.append(theEntity.getFhirVersion().name());
1152                                b.append("): ");
1153                                b.append(e.getMessage());
1154                                String msg = b.toString();
1155                                ourLog.error(msg, e);
1156                                throw new DataFormatException(Msg.code(928) + msg, e);
1157                        }
1158
1159                } else {
1160
1161                        retVal = (R) myContext.getResourceDefinition(theEntity.getResourceType()).newInstance();
1162
1163                }
1164
1165                // 5. fill MetaData
1166                if (retVal instanceof IResource) {
1167                        IResource res = (IResource) retVal;
1168                        retVal = populateResourceMetadataHapi(resourceType, theEntity, tagList, theForHistoryOperation, res, version);
1169                } else {
1170                        IAnyResource res = (IAnyResource) retVal;
1171                        retVal = populateResourceMetadataRi(resourceType, theEntity, tagList, theForHistoryOperation, res, version);
1172                }
1173
1174                // 6. Handle source (provenance)
1175                if (isNotBlank(provenanceRequestId) || isNotBlank(provenanceSourceUri)) {
1176                        String sourceString = cleanProvenanceSourceUri(provenanceSourceUri)
1177                                + (isNotBlank(provenanceRequestId) ? "#" : "")
1178                                + defaultString(provenanceRequestId);
1179
1180                        MetaUtil.setSource(myContext, retVal, sourceString);
1181
1182                }
1183
1184                // 7. Add partition information
1185                if (myPartitionSettings.isPartitioningEnabled()) {
1186                        PartitionablePartitionId partitionId = theEntity.getPartitionId();
1187                        if (partitionId != null && partitionId.getPartitionId() != null) {
1188                                PartitionEntity persistedPartition = myPartitionLookupSvc.getPartitionById(partitionId.getPartitionId());
1189                                retVal.setUserData(Constants.RESOURCE_PARTITION_ID, persistedPartition.toRequestPartitionId());
1190                        } else {
1191                                retVal.setUserData(Constants.RESOURCE_PARTITION_ID, null);
1192                        }
1193                }
1194
1195                return retVal;
1196        }
1197
1198        public String toResourceName(Class<? extends IBaseResource> theResourceType) {
1199                return myContext.getResourceType(theResourceType);
1200        }
1201
1202        String toResourceName(IBaseResource theResource) {
1203                return myContext.getResourceType(theResource);
1204        }
1205
1206        protected ResourceTable updateEntityForDelete(RequestDetails theRequest, TransactionDetails theTransactionDetails, ResourceTable entity) {
1207                Date updateTime = new Date();
1208                return updateEntity(theRequest, null, entity, updateTime, true, true, theTransactionDetails, false, true);
1209        }
1210
1211        @VisibleForTesting
1212        public void setEntityManager(EntityManager theEntityManager) {
1213                myEntityManager = theEntityManager;
1214        }
1215
1216        @VisibleForTesting
1217        public void setSearchParamWithInlineReferencesExtractor(SearchParamWithInlineReferencesExtractor theSearchParamWithInlineReferencesExtractor) {
1218                mySearchParamWithInlineReferencesExtractor = theSearchParamWithInlineReferencesExtractor;
1219        }
1220
1221        @VisibleForTesting
1222        public void setResourceHistoryTableDao(IResourceHistoryTableDao theResourceHistoryTableDao) {
1223                myResourceHistoryTableDao = theResourceHistoryTableDao;
1224        }
1225
1226        @VisibleForTesting
1227        public void setDaoSearchParamSynchronizer(DaoSearchParamSynchronizer theDaoSearchParamSynchronizer) {
1228                myDaoSearchParamSynchronizer = theDaoSearchParamSynchronizer;
1229        }
1230
1231        private void verifyMatchUrlForConditionalCreate(IBaseResource theResource, String theIfNoneExist, ResourceTable entity, ResourceIndexedSearchParams theParams) {
1232                // Make sure that the match URL was actually appropriate for the supplied resource
1233                InMemoryMatchResult outcome = myInMemoryResourceMatcher.match(theIfNoneExist, theResource, theParams);
1234                if (outcome.supported() && !outcome.matched()) {
1235                        throw new InvalidRequestException(Msg.code(929) + "Failed to process conditional create. The supplied resource did not satisfy the conditional URL.");
1236                }
1237        }
1238
1239
1240        @SuppressWarnings("unchecked")
1241        @Override
1242        public ResourceTable updateEntity(RequestDetails theRequest, final IBaseResource theResource, IBasePersistedResource
1243                theEntity, Date theDeletedTimestampOrNull, boolean thePerformIndexing,
1244                                                                                                 boolean theUpdateVersion, TransactionDetails theTransactionDetails, boolean theForceUpdate, boolean theCreateNewHistoryEntry) {
1245                Validate.notNull(theEntity);
1246                Validate.isTrue(theDeletedTimestampOrNull != null || theResource != null, "Must have either a resource[%s] or a deleted timestamp[%s] for resource PID[%s]", theDeletedTimestampOrNull != null, theResource != null, theEntity.getPersistentId());
1247
1248                ourLog.debug("Starting entity update");
1249
1250                ResourceTable entity = (ResourceTable) theEntity;
1251
1252                /*
1253                 * This should be the very first thing..
1254                 */
1255                if (theResource != null) {
1256                        if (thePerformIndexing) {
1257                                if (!ourValidationDisabledForUnitTest) {
1258                                        validateResourceForStorage((T) theResource, entity);
1259                                }
1260                        }
1261                        if (!StringUtils.isBlank(entity.getResourceType())) {
1262                                validateIncomingResourceTypeMatchesExisting(theResource, entity);
1263                        }
1264                }
1265
1266                if (entity.getPublished() == null) {
1267                        ourLog.debug("Entity has published time: {}", theTransactionDetails.getTransactionDate());
1268                        entity.setPublished(theTransactionDetails.getTransactionDate());
1269                }
1270
1271                ResourceIndexedSearchParams existingParams = null;
1272
1273                ResourceIndexedSearchParams newParams = null;
1274
1275                EncodedResource changed;
1276                if (theDeletedTimestampOrNull != null) {
1277                        // DELETE
1278
1279                        entity.setDeleted(theDeletedTimestampOrNull);
1280                        entity.setUpdated(theDeletedTimestampOrNull);
1281                        entity.setNarrativeText(null);
1282                        entity.setContentText(null);
1283                        entity.setHashSha256(null);
1284                        entity.setIndexStatus(INDEX_STATUS_INDEXED);
1285                        changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
1286
1287                } else {
1288
1289                        // CREATE or UPDATE
1290
1291                        IdentityHashMap<ResourceTable, ResourceIndexedSearchParams> existingSearchParams = theTransactionDetails.getOrCreateUserData(HapiTransactionService.XACT_USERDATA_KEY_EXISTING_SEARCH_PARAMS, () -> new IdentityHashMap<>());
1292                        existingParams = existingSearchParams.get(entity);
1293                        if (existingParams == null) {
1294                                existingParams = new ResourceIndexedSearchParams(entity);
1295                                existingSearchParams.put(entity, existingParams);
1296                        }
1297                        entity.setDeleted(null);
1298
1299                        // TODO: is this IF statement always true? Try removing it
1300                        if (thePerformIndexing || ((ResourceTable) theEntity).getVersion() == 1) {
1301
1302                                newParams = new ResourceIndexedSearchParams();
1303
1304                                RequestPartitionId requestPartitionId;
1305                                if (!myPartitionSettings.isPartitioningEnabled()) {
1306                                        requestPartitionId = RequestPartitionId.allPartitions();
1307                                } else if (entity.getPartitionId() != null) {
1308                                        requestPartitionId = entity.getPartitionId().toPartitionId();
1309                                } else {
1310                                        requestPartitionId = RequestPartitionId.defaultPartition();
1311                                }
1312
1313                                failIfPartitionMismatch(theRequest, entity);
1314                                mySearchParamWithInlineReferencesExtractor.populateFromResource(requestPartitionId, newParams, theTransactionDetails, entity, theResource, existingParams, theRequest, thePerformIndexing);
1315
1316                                changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, true);
1317
1318                                if (theForceUpdate) {
1319                                        changed.setChanged(true);
1320                                }
1321
1322                                if (changed.isChanged()) {
1323
1324                                        // Make sure that the match URL was actually appropriate for the supplied
1325                                        // resource. We only do this for version 1 right now since technically it
1326                                        // is possible (and legal) for someone to be using a conditional update
1327                                        // to match a resource and then update it in a way that it no longer
1328                                        // matches. We could certainly make this configurable though in the
1329                                        // future.
1330                                        if (entity.getVersion() <= 1L && entity.getCreatedByMatchUrl() != null && thePerformIndexing) {
1331                                                verifyMatchUrlForConditionalCreate(theResource, entity.getCreatedByMatchUrl(), entity, newParams);
1332                                        }
1333
1334                                        entity.setUpdated(theTransactionDetails.getTransactionDate());
1335                                        newParams.populateResourceTableSearchParamsPresentFlags(entity);
1336                                        entity.setIndexStatus(INDEX_STATUS_INDEXED);
1337                                }
1338
1339                                if (myFulltextSearchSvc != null && !myFulltextSearchSvc.isDisabled()) {
1340                                        populateFullTextFields(myContext, theResource, entity, newParams);
1341                                }
1342
1343                        } else {
1344
1345                                changed = populateResourceIntoEntity(theTransactionDetails, theRequest, theResource, entity, false);
1346
1347                                entity.setUpdated(theTransactionDetails.getTransactionDate());
1348                                entity.setIndexStatus(null);
1349
1350                        }
1351
1352                }
1353
1354                if (thePerformIndexing && changed != null && !changed.isChanged() && !theForceUpdate && myConfig.isSuppressUpdatesWithNoChange() && (entity.getVersion() > 1 || theUpdateVersion)) {
1355                        ourLog.debug("Resource {} has not changed", entity.getIdDt().toUnqualified().getValue());
1356                        if (theResource != null) {
1357                                updateResourceMetadata(entity, theResource);
1358                        }
1359                        entity.setUnchangedInCurrentOperation(true);
1360                        return entity;
1361                }
1362
1363                if (theUpdateVersion) {
1364                        long newVersion = entity.getVersion() + 1;
1365                        entity.setVersion(newVersion);
1366                }
1367
1368                /*
1369                 * Save the resource itself
1370                 */
1371                if (entity.getId() == null) {
1372                        myEntityManager.persist(entity);
1373
1374                        if (entity.getForcedId() != null) {
1375                                myEntityManager.persist(entity.getForcedId());
1376                        }
1377
1378                        postPersist(entity, (T) theResource);
1379
1380                } else if (entity.getDeleted() != null) {
1381                        entity = myEntityManager.merge(entity);
1382
1383                        postDelete(entity);
1384
1385                } else {
1386                        entity = myEntityManager.merge(entity);
1387
1388                        postUpdate(entity, (T) theResource);
1389                }
1390
1391                if (theCreateNewHistoryEntry) {
1392                        createHistoryEntry(theRequest, theResource, entity, changed);
1393                }
1394
1395                /*
1396                 * Update the "search param present" table which is used for the
1397                 * ?foo:missing=true queries
1398                 *
1399                 * Note that we're only populating this for reference params
1400                 * because the index tables for all other types have a MISSING column
1401                 * right on them for handling the :missing queries. We can't use the
1402                 * index table for resource links (reference indexes) because we index
1403                 * those by path and not by parameter name.
1404                 */
1405                if (thePerformIndexing && newParams != null) {
1406                        Map<String, Boolean> searchParamPresenceMap = getSearchParamPresenceMap(entity, newParams);
1407
1408                        AddRemoveCount presenceCount = mySearchParamPresenceSvc.updatePresence(entity, searchParamPresenceMap);
1409
1410                        // Interceptor broadcast: JPA_PERFTRACE_INFO
1411                        if (!presenceCount.isEmpty()) {
1412                                if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequest)) {
1413                                        StorageProcessingMessage message = new StorageProcessingMessage();
1414                                        message.setMessage("For " + entity.getIdDt().toUnqualifiedVersionless().getValue() + " added " + presenceCount.getAddCount() + " and removed " + presenceCount.getRemoveCount() + " resource search parameter presence entries");
1415                                        HookParams params = new HookParams()
1416                                                .add(RequestDetails.class, theRequest)
1417                                                .addIfMatchesType(ServletRequestDetails.class, theRequest)
1418                                                .add(StorageProcessingMessage.class, message);
1419                                        CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params);
1420                                }
1421                        }
1422
1423                }
1424
1425                /*
1426                 * Indexing
1427                 */
1428                if (thePerformIndexing) {
1429                        if (newParams == null) {
1430                                myExpungeService.deleteAllSearchParams(new ResourcePersistentId(entity.getId()));
1431                        } else {
1432
1433                                // Synchronize search param indexes
1434                                AddRemoveCount searchParamAddRemoveCount = myDaoSearchParamSynchronizer.synchronizeSearchParamsToDatabase(newParams, entity, existingParams);
1435
1436                                newParams.populateResourceTableParamCollections(entity);
1437
1438                                // Interceptor broadcast: JPA_PERFTRACE_INFO
1439                                if (!searchParamAddRemoveCount.isEmpty()) {
1440                                        if (CompositeInterceptorBroadcaster.hasHooks(Pointcut.JPA_PERFTRACE_INFO, myInterceptorBroadcaster, theRequest)) {
1441                                                StorageProcessingMessage message = new StorageProcessingMessage();
1442                                                message.setMessage("For " + entity.getIdDt().toUnqualifiedVersionless().getValue() + " added " + searchParamAddRemoveCount.getAddCount() + " and removed " + searchParamAddRemoveCount.getRemoveCount() + " resource search parameter index entries");
1443                                                HookParams params = new HookParams()
1444                                                        .add(RequestDetails.class, theRequest)
1445                                                        .addIfMatchesType(ServletRequestDetails.class, theRequest)
1446                                                        .add(StorageProcessingMessage.class, message);
1447                                                CompositeInterceptorBroadcaster.doCallHooks(myInterceptorBroadcaster, theRequest, Pointcut.JPA_PERFTRACE_INFO, params);
1448                                        }
1449                                }
1450
1451                                // Synchronize composite params
1452                                mySearchParamWithInlineReferencesExtractor.storeUniqueComboParameters(newParams, entity, existingParams);
1453                        }
1454                }
1455
1456                if (theResource != null) {
1457                        updateResourceMetadata(entity, theResource);
1458                }
1459
1460
1461                return entity;
1462        }
1463
1464        @NotNull
1465        private Map<String, Boolean> getSearchParamPresenceMap(ResourceTable entity, ResourceIndexedSearchParams newParams) {
1466                Map<String, Boolean> retval = new HashMap<>();
1467
1468                for (String nextKey : newParams.getPopulatedResourceLinkParameters()) {
1469                        retval.put(nextKey, Boolean.TRUE);
1470                }
1471
1472                ResourceSearchParams activeSearchParams = mySearchParamRegistry.getActiveSearchParams(entity.getResourceType());
1473                activeSearchParams.getReferenceSearchParamNames().forEach(key -> {
1474                        if (!retval.containsKey(key)) {
1475                                retval.put(key, Boolean.FALSE);
1476                        }
1477                });
1478                return retval;
1479        }
1480
1481        /**
1482         * TODO eventually consider refactoring this to be part of an interceptor.
1483         *
1484         * Throws an exception if the partition of the request, and the partition of the existing entity do not match.
1485         * @param theRequest the request.
1486         * @param entity the existing entity.
1487         */
1488        private void failIfPartitionMismatch(RequestDetails theRequest, ResourceTable entity) {
1489                if (myPartitionSettings.isPartitioningEnabled() && theRequest != null && theRequest.getTenantId() != null && entity.getPartitionId() != null &&
1490                                theRequest.getTenantId() != ALL_PARTITIONS_NAME) {
1491                        PartitionEntity partitionEntity = myPartitionLookupSvc.getPartitionByName(theRequest.getTenantId());
1492                        //partitionEntity should never be null
1493                        if (partitionEntity != null && !partitionEntity.getId().equals(entity.getPartitionId().getPartitionId())) {
1494                                throw new InvalidRequestException(Msg.code(2079) + "Resource " + entity.getResourceType() + "/" + entity.getId() + " is not known");
1495                        }
1496                }
1497        }
1498
1499        private void createHistoryEntry(RequestDetails theRequest, IBaseResource theResource, ResourceTable theEntity, EncodedResource theChanged) {
1500                boolean versionedTags = getConfig().getTagStorageMode() == DaoConfig.TagStorageModeEnum.VERSIONED;
1501                final ResourceHistoryTable historyEntry = theEntity.toHistory(versionedTags);
1502                historyEntry.setEncoding(theChanged.getEncoding());
1503                historyEntry.setResource(theChanged.getResourceBinary());
1504                historyEntry.setResourceTextVc(theChanged.getResourceText());
1505
1506                ourLog.debug("Saving history entry {}", historyEntry.getIdDt());
1507                myResourceHistoryTableDao.save(historyEntry);
1508                theEntity.setCurrentVersionEntity(historyEntry);
1509
1510                // Save resource source
1511                String source = null;
1512                String requestId = theRequest != null ? theRequest.getRequestId() : null;
1513                if (theResource != null) {
1514                        if (myContext.getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.R4)) {
1515                                IBaseMetaType meta = theResource.getMeta();
1516                                source = MetaUtil.getSource(myContext, meta);
1517                        }
1518                        if (myContext.getVersion().getVersion().equals(FhirVersionEnum.DSTU3)) {
1519                                source = ((IBaseHasExtensions) theResource.getMeta())
1520                                        .getExtension()
1521                                        .stream()
1522                                        .filter(t -> HapiExtensions.EXT_META_SOURCE.equals(t.getUrl()))
1523                                        .filter(t -> t.getValue() instanceof IPrimitiveType)
1524                                        .map(t -> ((IPrimitiveType<?>) t.getValue()).getValueAsString())
1525                                        .findFirst()
1526                                        .orElse(null);
1527                        }
1528                }
1529
1530                boolean haveSource = isNotBlank(source) && myConfig.getStoreMetaSourceInformation().isStoreSourceUri();
1531                boolean haveRequestId = isNotBlank(requestId) && myConfig.getStoreMetaSourceInformation().isStoreRequestId();
1532                if (haveSource || haveRequestId) {
1533                        ResourceHistoryProvenanceEntity provenance = new ResourceHistoryProvenanceEntity();
1534                        provenance.setResourceHistoryTable(historyEntry);
1535                        provenance.setResourceTable(theEntity);
1536                        provenance.setPartitionId(theEntity.getPartitionId());
1537                        if (haveRequestId) {
1538                                provenance.setRequestId(left(requestId, Constants.REQUEST_ID_LENGTH));
1539                        }
1540                        if (haveSource) {
1541                                provenance.setSourceUri(source);
1542                        }
1543                        myEntityManager.persist(provenance);
1544                }
1545        }
1546
1547        private void validateIncomingResourceTypeMatchesExisting(IBaseResource theResource, ResourceTable entity) {
1548                String resourceType = myContext.getResourceType(theResource);
1549                if (!resourceType.equals(entity.getResourceType())) {
1550                        throw new UnprocessableEntityException(Msg.code(930) + "Existing resource ID[" + entity.getIdDt().toUnqualifiedVersionless() + "] is of type[" + entity.getResourceType() + "] - Cannot update with [" + resourceType + "]");
1551                }
1552        }
1553
1554        @Override
1555        public ResourceTable updateInternal(RequestDetails theRequestDetails, T theResource, boolean thePerformIndexing, boolean theForceUpdateVersion,
1556                                                                                                        IBasePersistedResource theEntity, IIdType theResourceId, IBaseResource theOldResource, TransactionDetails theTransactionDetails) {
1557
1558                ResourceTable entity = (ResourceTable) theEntity;
1559
1560                // We'll update the resource ID with the correct version later but for
1561                // now at least set it to something useful for the interceptors
1562                theResource.setId(entity.getIdDt());
1563
1564                // Notify interceptors
1565                ActionRequestDetails requestDetails;
1566                if (theRequestDetails != null && theRequestDetails.getServer() != null) {
1567                        requestDetails = new ActionRequestDetails(theRequestDetails, theResource, theResourceId.getResourceType(), theResourceId);
1568                        notifyInterceptors(RestOperationTypeEnum.UPDATE, requestDetails);
1569                }
1570
1571                // Notify IServerOperationInterceptors about pre-action call
1572                HookParams hookParams = new HookParams()
1573                        .add(IBaseResource.class, theOldResource)
1574                        .add(IBaseResource.class, theResource)
1575                        .add(RequestDetails.class, theRequestDetails)
1576                        .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
1577                        .add(TransactionDetails.class, theTransactionDetails);
1578                doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRESTORAGE_RESOURCE_UPDATED, hookParams);
1579
1580                // Perform update
1581                ResourceTable savedEntity = updateEntity(theRequestDetails, theResource, entity, null, thePerformIndexing, thePerformIndexing, theTransactionDetails, theForceUpdateVersion, thePerformIndexing);
1582
1583                /*
1584                 * If we aren't indexing (meaning we're probably executing a sub-operation within a transaction),
1585                 * we'll manually increase the version. This is important because we want the updated version number
1586                 * to be reflected in the resource shared with interceptors
1587                 */
1588                if (!thePerformIndexing && !savedEntity.isUnchangedInCurrentOperation() && !ourDisableIncrementOnUpdateForUnitTest) {
1589                        if (theResourceId.hasVersionIdPart() == false) {
1590                                theResourceId = theResourceId.withVersion(Long.toString(savedEntity.getVersion()));
1591                        }
1592                        incrementId(theResource, savedEntity, theResourceId);
1593                }
1594
1595                // Update version/lastUpdated so that interceptors see the correct version
1596                updateResourceMetadata(savedEntity, theResource);
1597
1598                // Populate the PID in the resource so it is available to hooks
1599                addPidToResource(savedEntity, theResource);
1600
1601                // Notify interceptors
1602                if (!savedEntity.isUnchangedInCurrentOperation()) {
1603                        hookParams = new HookParams()
1604                                .add(IBaseResource.class, theOldResource)
1605                                .add(IBaseResource.class, theResource)
1606                                .add(RequestDetails.class, theRequestDetails)
1607                                .addIfMatchesType(ServletRequestDetails.class, theRequestDetails)
1608                                .add(TransactionDetails.class, theTransactionDetails)
1609                                .add(InterceptorInvocationTimingEnum.class, theTransactionDetails.getInvocationTiming(Pointcut.STORAGE_PRECOMMIT_RESOURCE_CREATED));
1610                        doCallHooks(theTransactionDetails, theRequestDetails, Pointcut.STORAGE_PRECOMMIT_RESOURCE_UPDATED, hookParams);
1611                }
1612
1613                return savedEntity;
1614        }
1615
1616        protected void addPidToResource(IBasePersistedResource theEntity, IBaseResource theResource) {
1617                if (theResource instanceof IAnyResource) {
1618                        IDao.RESOURCE_PID.put((IAnyResource) theResource, theEntity.getPersistentId().getIdAsLong());
1619                } else if (theResource instanceof IResource) {
1620                        IDao.RESOURCE_PID.put((IResource) theResource, theEntity.getPersistentId().getIdAsLong());
1621                }
1622        }
1623
1624        protected void updateResourceMetadata(IBaseResourceEntity theEntity, IBaseResource theResource) {
1625                IIdType id = theEntity.getIdDt();
1626                if (getContext().getVersion().getVersion().isRi()) {
1627                        id = getContext().getVersion().newIdType().setValue(id.getValue());
1628                }
1629
1630                if (id.hasResourceType() == false) {
1631                        id = id.withResourceType(theEntity.getResourceType());
1632                }
1633
1634                theResource.setId(id);
1635                if (theResource instanceof IResource) {
1636                        ResourceMetadataKeyEnum.VERSION.put((IResource) theResource, id.getVersionIdPart());
1637                        ResourceMetadataKeyEnum.UPDATED.put((IResource) theResource, theEntity.getUpdated());
1638                } else {
1639                        IBaseMetaType meta = theResource.getMeta();
1640                        meta.setVersionId(id.getVersionIdPart());
1641                        meta.setLastUpdated(theEntity.getUpdatedDate());
1642                }
1643        }
1644
1645        private void validateChildReferenceTargetTypes(IBase theElement, String thePath) {
1646                if (theElement == null) {
1647                        return;
1648                }
1649                BaseRuntimeElementDefinition<?> def = myContext.getElementDefinition(theElement.getClass());
1650                if (!(def instanceof BaseRuntimeElementCompositeDefinition)) {
1651                        return;
1652                }
1653
1654                BaseRuntimeElementCompositeDefinition<?> cdef = (BaseRuntimeElementCompositeDefinition<?>) def;
1655                for (BaseRuntimeChildDefinition nextChildDef : cdef.getChildren()) {
1656
1657                        List<IBase> values = nextChildDef.getAccessor().getValues(theElement);
1658                        if (values == null || values.isEmpty()) {
1659                                continue;
1660                        }
1661
1662                        String newPath = thePath + "." + nextChildDef.getElementName();
1663
1664                        for (IBase nextChild : values) {
1665                                validateChildReferenceTargetTypes(nextChild, newPath);
1666                        }
1667
1668                        if (nextChildDef instanceof RuntimeChildResourceDefinition) {
1669                                RuntimeChildResourceDefinition nextChildDefRes = (RuntimeChildResourceDefinition) nextChildDef;
1670                                Set<String> validTypes = new HashSet<>();
1671                                boolean allowAny = false;
1672                                for (Class<? extends IBaseResource> nextValidType : nextChildDefRes.getResourceTypes()) {
1673                                        if (nextValidType.isInterface()) {
1674                                                allowAny = true;
1675                                                break;
1676                                        }
1677                                        validTypes.add(getContext().getResourceType(nextValidType));
1678                                }
1679
1680                                if (allowAny) {
1681                                        continue;
1682                                }
1683
1684                                if (getConfig().isEnforceReferenceTargetTypes()) {
1685                                        for (IBase nextChild : values) {
1686                                                IBaseReference nextRef = (IBaseReference) nextChild;
1687                                                IIdType referencedId = nextRef.getReferenceElement();
1688                                                if (!isBlank(referencedId.getResourceType())) {
1689                                                        if (!isLogicalReference(referencedId)) {
1690                                                                if (!referencedId.getValue().contains("?")) {
1691                                                                        if (!validTypes.contains(referencedId.getResourceType())) {
1692                                                                                throw new UnprocessableEntityException(Msg.code(931) + "Invalid reference found at path '" + newPath + "'. Resource type '" + referencedId.getResourceType() + "' is not valid for this path");
1693                                                                        }
1694                                                                }
1695                                                        }
1696                                                }
1697                                        }
1698                                }
1699
1700                        }
1701                }
1702        }
1703
1704        protected void validateMetaCount(int theMetaCount) {
1705                if (myConfig.getResourceMetaCountHardLimit() != null) {
1706                        if (theMetaCount > myConfig.getResourceMetaCountHardLimit()) {
1707                                throw new UnprocessableEntityException(Msg.code(932) + "Resource contains " + theMetaCount + " meta entries (tag/profile/security label), maximum is " + myConfig.getResourceMetaCountHardLimit());
1708                        }
1709                }
1710        }
1711
1712        /**
1713         * 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
1714         * "subsetted" tag and rejects resources which have it. Subclasses should call the superclass implementation to preserve this check.
1715         *
1716         * @param theResource     The resource that is about to be persisted
1717         * @param theEntityToSave TODO
1718         */
1719        protected void validateResourceForStorage(T theResource, ResourceTable theEntityToSave) {
1720                Object tag = null;
1721
1722                int totalMetaCount = 0;
1723
1724                if (theResource instanceof IResource) {
1725                        IResource res = (IResource) theResource;
1726                        TagList tagList = ResourceMetadataKeyEnum.TAG_LIST.get(res);
1727                        if (tagList != null) {
1728                                tag = tagList.getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE);
1729                                totalMetaCount += tagList.size();
1730                        }
1731                        List<IdDt> profileList = ResourceMetadataKeyEnum.PROFILES.get(res);
1732                        if (profileList != null) {
1733                                totalMetaCount += profileList.size();
1734                        }
1735                } else {
1736                        IAnyResource res = (IAnyResource) theResource;
1737                        tag = res.getMeta().getTag(Constants.TAG_SUBSETTED_SYSTEM_DSTU3, Constants.TAG_SUBSETTED_CODE);
1738                        totalMetaCount += res.getMeta().getTag().size();
1739                        totalMetaCount += res.getMeta().getProfile().size();
1740                        totalMetaCount += res.getMeta().getSecurity().size();
1741                }
1742
1743                if (tag != null) {
1744                        throw new UnprocessableEntityException(Msg.code(933) + "Resource contains the 'subsetted' tag, and must not be stored as it may contain a subset of available data");
1745                }
1746
1747                if (getConfig().isEnforceReferenceTargetTypes()) {
1748                        String resName = getContext().getResourceType(theResource);
1749                        validateChildReferenceTargetTypes(theResource, resName);
1750                }
1751
1752                validateMetaCount(totalMetaCount);
1753
1754        }
1755
1756        @PostConstruct
1757        public void start() {
1758                // nothing yet
1759        }
1760
1761        @VisibleForTesting
1762        public void setDaoConfigForUnitTest(DaoConfig theDaoConfig) {
1763                myConfig = theDaoConfig;
1764        }
1765
1766        public void populateFullTextFields(final FhirContext theContext, final IBaseResource theResource, ResourceTable theEntity, ResourceIndexedSearchParams theNewParams) {
1767                if (theEntity.getDeleted() != null) {
1768                        theEntity.setNarrativeText(null);
1769                        theEntity.setContentText(null);
1770                } else {
1771                        theEntity.setNarrativeText(parseNarrativeTextIntoWords(theResource));
1772                        theEntity.setContentText(parseContentTextIntoWords(theContext, theResource));
1773                        if (myDaoConfig.isAdvancedLuceneIndexing()) {
1774                                ExtendedLuceneIndexData luceneIndexData = myFulltextSearchSvc.extractLuceneIndexData(theResource, theNewParams);
1775                                theEntity.setLuceneIndexData(luceneIndexData);
1776                        }
1777                }
1778        }
1779
1780        @VisibleForTesting
1781        public void setPartitionSettingsForUnitTest(PartitionSettings thePartitionSettings) {
1782                myPartitionSettings = thePartitionSettings;
1783        }
1784
1785        @Nonnull
1786        public static MemoryCacheService.TagDefinitionCacheKey toTagDefinitionMemoryCacheKey(TagTypeEnum theTagType, String theScheme, String theTerm) {
1787                return new MemoryCacheService.TagDefinitionCacheKey(theTagType, theScheme, theTerm);
1788        }
1789
1790        static String cleanProvenanceSourceUri(String theProvenanceSourceUri) {
1791                if (isNotBlank(theProvenanceSourceUri)) {
1792                        int hashIndex = theProvenanceSourceUri.indexOf('#');
1793                        if (hashIndex != -1) {
1794                                theProvenanceSourceUri = theProvenanceSourceUri.substring(0, hashIndex);
1795                        }
1796                }
1797                return defaultString(theProvenanceSourceUri);
1798        }
1799
1800        @SuppressWarnings("unchecked")
1801        public static String parseContentTextIntoWords(FhirContext theContext, IBaseResource theResource) {
1802
1803                Class<IPrimitiveType<String>> stringType = (Class<IPrimitiveType<String>>) theContext.getElementDefinition("string").getImplementingClass();
1804
1805                StringBuilder retVal = new StringBuilder();
1806                List<IPrimitiveType<String>> childElements = theContext.newTerser().getAllPopulatedChildElementsOfType(theResource, stringType);
1807                for (IPrimitiveType<String> nextType : childElements) {
1808                        if (stringType.equals(nextType.getClass())) {
1809                                String nextValue = nextType.getValueAsString();
1810                                if (isNotBlank(nextValue)) {
1811                                        retVal.append(nextValue.replace("\n", " ").replace("\r", " "));
1812                                        retVal.append("\n");
1813                                }
1814                        }
1815                }
1816                return retVal.toString();
1817        }
1818
1819        public static String decodeResource(byte[] theResourceBytes, ResourceEncodingEnum theResourceEncoding) {
1820                String resourceText = null;
1821                switch (theResourceEncoding) {
1822                        case JSON:
1823                                resourceText = new String(theResourceBytes, Charsets.UTF_8);
1824                                break;
1825                        case JSONC:
1826                                resourceText = GZipUtil.decompress(theResourceBytes);
1827                                break;
1828                        case DEL:
1829                                break;
1830                }
1831                return resourceText;
1832        }
1833
1834        public static String encodeResource(IBaseResource theResource, ResourceEncodingEnum theEncoding, List<String> theExcludeElements, FhirContext theContext) {
1835                IParser parser = theEncoding.newParser(theContext);
1836                parser.setDontEncodeElements(theExcludeElements);
1837                return parser.encodeResourceToString(theResource);
1838        }
1839
1840        private static String parseNarrativeTextIntoWords(IBaseResource theResource) {
1841
1842                StringBuilder b = new StringBuilder();
1843                if (theResource instanceof IResource) {
1844                        IResource resource = (IResource) theResource;
1845                        List<XMLEvent> xmlEvents = XmlUtil.parse(resource.getText().getDiv().getValue());
1846                        if (xmlEvents != null) {
1847                                for (XMLEvent next : xmlEvents) {
1848                                        if (next.isCharacters()) {
1849                                                Characters characters = next.asCharacters();
1850                                                b.append(characters.getData()).append(" ");
1851                                        }
1852                                }
1853                        }
1854                } else if (theResource instanceof IDomainResource) {
1855                        IDomainResource resource = (IDomainResource) theResource;
1856                        try {
1857                                String divAsString = resource.getText().getDivAsString();
1858                                List<XMLEvent> xmlEvents = XmlUtil.parse(divAsString);
1859                                if (xmlEvents != null) {
1860                                        for (XMLEvent next : xmlEvents) {
1861                                                if (next.isCharacters()) {
1862                                                        Characters characters = next.asCharacters();
1863                                                        b.append(characters.getData()).append(" ");
1864                                                }
1865                                        }
1866                                }
1867                        } catch (Exception e) {
1868                                throw new DataFormatException(Msg.code(934) + "Unable to convert DIV to string", e);
1869                        }
1870
1871                }
1872                return b.toString();
1873        }
1874
1875        @VisibleForTesting
1876        public static void setDisableIncrementOnUpdateForUnitTest(boolean theDisableIncrementOnUpdateForUnitTest) {
1877                ourDisableIncrementOnUpdateForUnitTest = theDisableIncrementOnUpdateForUnitTest;
1878        }
1879
1880        /**
1881         * Do not call this method outside of unit tests
1882         */
1883        @VisibleForTesting
1884        public static void setValidationDisabledForUnitTest(boolean theValidationDisabledForUnitTest) {
1885                ourValidationDisabledForUnitTest = theValidationDisabledForUnitTest;
1886        }
1887
1888        private static List<BaseCodingDt> toBaseCodingList(List<IBaseCoding> theSecurityLabels) {
1889                ArrayList<BaseCodingDt> retVal = new ArrayList<>(theSecurityLabels.size());
1890                for (IBaseCoding next : theSecurityLabels) {
1891                        retVal.add((BaseCodingDt) next);
1892                }
1893                return retVal;
1894        }
1895
1896        public static void validateResourceType(BaseHasResource theEntity, String theResourceName) {
1897                if (!theResourceName.equals(theEntity.getResourceType())) {
1898                        throw new ResourceNotFoundException(Msg.code(935) + "Resource with ID " + theEntity.getIdDt().getIdPart() + " exists but it is not of type " + theResourceName + ", found resource of type " + theEntity.getResourceType());
1899                }
1900        }
1901
1902        private class AddTagDefinitionToCacheAfterCommitSynchronization implements TransactionSynchronization {
1903
1904                private final TagDefinition myTagDefinition;
1905                private final MemoryCacheService.TagDefinitionCacheKey myKey;
1906
1907                public AddTagDefinitionToCacheAfterCommitSynchronization(MemoryCacheService.TagDefinitionCacheKey theKey, TagDefinition theTagDefinition) {
1908                        myTagDefinition = theTagDefinition;
1909                        myKey = theKey;
1910                }
1911
1912                @Override
1913                public void afterCommit() {
1914                        myMemoryCacheService.put(MemoryCacheService.CacheEnum.TAG_DEFINITION, myKey, myTagDefinition);
1915                }
1916        }
1917
1918}