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