001/*
002 * #%L
003 * HAPI FHIR JPA Model
004 * %%
005 * Copyright (C) 2014 - 2025 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.jpa.model.entity;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.jpa.model.cross.IBasePersistedResource;
024import ca.uhn.fhir.jpa.model.dao.JpaPid;
025import ca.uhn.fhir.jpa.model.dao.JpaPidIdentifierBridge;
026import ca.uhn.fhir.jpa.model.dao.JpaPidValueBridge;
027import ca.uhn.fhir.jpa.model.search.ExtendedHSearchIndexData;
028import ca.uhn.fhir.jpa.model.search.ResourceTableRoutingBinder;
029import ca.uhn.fhir.jpa.model.search.SearchParamTextPropertyBinder;
030import ca.uhn.fhir.model.primitive.IdDt;
031import ca.uhn.fhir.rest.api.Constants;
032import com.google.common.annotations.VisibleForTesting;
033import jakarta.annotation.Nonnull;
034import jakarta.persistence.CascadeType;
035import jakarta.persistence.Column;
036import jakarta.persistence.EmbeddedId;
037import jakarta.persistence.Entity;
038import jakarta.persistence.EnumType;
039import jakarta.persistence.Enumerated;
040import jakarta.persistence.FetchType;
041import jakarta.persistence.Index;
042import jakarta.persistence.NamedEntityGraph;
043import jakarta.persistence.OneToMany;
044import jakarta.persistence.PostPersist;
045import jakarta.persistence.PrePersist;
046import jakarta.persistence.PreUpdate;
047import jakarta.persistence.Table;
048import jakarta.persistence.Transient;
049import jakarta.persistence.UniqueConstraint;
050import jakarta.persistence.Version;
051import org.apache.commons.lang3.builder.ToStringBuilder;
052import org.apache.commons.lang3.builder.ToStringStyle;
053import org.hibernate.Session;
054import org.hibernate.annotations.GenerationTime;
055import org.hibernate.annotations.GeneratorType;
056import org.hibernate.annotations.JdbcTypeCode;
057import org.hibernate.annotations.OptimisticLock;
058import org.hibernate.search.engine.backend.types.Projectable;
059import org.hibernate.search.engine.backend.types.Searchable;
060import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.IdentifierBridgeRef;
061import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.PropertyBinderRef;
062import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.RoutingBinderRef;
063import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.ValueBridgeRef;
064import org.hibernate.search.mapper.pojo.mapping.definition.annotation.DocumentId;
065import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
066import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField;
067import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed;
068import org.hibernate.search.mapper.pojo.mapping.definition.annotation.IndexingDependency;
069import org.hibernate.search.mapper.pojo.mapping.definition.annotation.ObjectPath;
070import org.hibernate.search.mapper.pojo.mapping.definition.annotation.PropertyBinding;
071import org.hibernate.search.mapper.pojo.mapping.definition.annotation.PropertyValue;
072import org.hibernate.tuple.ValueGenerator;
073import org.hibernate.type.SqlTypes;
074import org.hl7.fhir.instance.model.api.IIdType;
075import org.hl7.fhir.r4.model.InstantType;
076
077import java.io.Serializable;
078import java.time.LocalDate;
079import java.util.ArrayList;
080import java.util.Collection;
081import java.util.HashSet;
082import java.util.Objects;
083import java.util.Set;
084import java.util.stream.Collectors;
085
086import static ca.uhn.fhir.jpa.model.entity.ResourceTable.IDX_RES_TYPE_FHIR_ID;
087
088@Indexed(routingBinder = @RoutingBinderRef(type = ResourceTableRoutingBinder.class))
089@Entity
090@Table(
091                name = ResourceTable.HFJ_RESOURCE,
092                uniqueConstraints = {
093                        @UniqueConstraint(
094                                        name = IDX_RES_TYPE_FHIR_ID,
095                                        columnNames = {"PARTITION_ID", "RES_TYPE", "FHIR_ID"})
096                },
097                indexes = {
098                        // Do not reuse previously used index name: IDX_INDEXSTATUS, IDX_RES_TYPE
099                        @Index(name = "IDX_RES_DATE", columnList = BaseHasResource.RES_UPDATED),
100                        @Index(name = "IDX_RES_FHIR_ID", columnList = "FHIR_ID"),
101                        @Index(
102                                        name = "IDX_RES_TYPE_DEL_UPDATED",
103                                        columnList = "RES_TYPE,RES_DELETED_AT,RES_UPDATED,PARTITION_ID,RES_ID"),
104                        @Index(name = "IDX_RES_RESID_UPDATED", columnList = "RES_ID, RES_UPDATED, PARTITION_ID")
105                })
106@NamedEntityGraph(name = "Resource.noJoins")
107public class ResourceTable extends BaseHasResource<JpaPid> implements Serializable, IBasePersistedResource<JpaPid> {
108        public static final int RESTYPE_LEN = 40;
109        public static final String HFJ_RESOURCE = "HFJ_RESOURCE";
110        public static final String RES_TYPE = "RES_TYPE";
111        public static final String FHIR_ID = "FHIR_ID";
112        private static final int MAX_LANGUAGE_LENGTH = 20;
113        private static final long serialVersionUID = 1L;
114        public static final int MAX_FORCED_ID_LENGTH = 100;
115        public static final String IDX_RES_TYPE_FHIR_ID = "IDX_RES_TYPE_FHIR_ID";
116
117        /**
118         * Holds the narrative text only - Used for Fulltext searching but not directly stored in the DB
119         * Note the extra config needed in HS6 for indexing transient props:
120         * https://docs.jboss.org/hibernate/search/6.0/migration/html_single/#indexed-transient-requires-configuration
121         * <p>
122         * Note that we depend on `myVersion` updated for this field to be indexed.
123         */
124        @Transient
125        @FullTextField(
126                        name = "myContentText",
127                        searchable = Searchable.YES,
128                        projectable = Projectable.YES,
129                        analyzer = "standardAnalyzer")
130        @OptimisticLock(excluded = true)
131        @IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "myVersion")))
132        private String myContentText;
133
134        @Column(name = "HASH_SHA256", length = 64, nullable = true)
135        @OptimisticLock(excluded = true)
136        private String myHashSha256;
137
138        @Column(name = "SP_HAS_LINKS", nullable = false)
139        @OptimisticLock(excluded = true)
140        private boolean myHasLinks;
141
142        @EmbeddedId
143        @DocumentId(identifierBridge = @IdentifierBridgeRef(type = JpaPidIdentifierBridge.class))
144        @GenericField(
145                        name = "myId",
146                        projectable = Projectable.YES,
147                        valueBridge = @ValueBridgeRef(type = JpaPidValueBridge.class))
148        private JpaPid myPid;
149
150        @Column(name = PartitionablePartitionId.PARTITION_ID, nullable = true, insertable = false, updatable = false)
151        private Integer myPartitionIdValue;
152
153        @Column(name = PartitionablePartitionId.PARTITION_DATE, nullable = true)
154        private LocalDate myPartitionDateValue;
155
156        @Column(name = "SP_INDEX_STATUS", nullable = true)
157        @Enumerated(EnumType.ORDINAL)
158        @JdbcTypeCode(SqlTypes.TINYINT)
159        @OptimisticLock(excluded = true)
160        private EntityIndexStatusEnum myIndexStatus;
161
162        // TODO: Removed in 5.5.0. Drop in a future release.
163        @Column(name = "RES_LANGUAGE", length = MAX_LANGUAGE_LENGTH, nullable = true)
164        @OptimisticLock(excluded = true)
165        private String myLanguage;
166
167        /**
168         * Holds the narrative text only - Used for Fulltext searching but not directly stored in the DB
169         */
170        @Transient()
171        @FullTextField(
172                        name = "myNarrativeText",
173                        searchable = Searchable.YES,
174                        projectable = Projectable.YES,
175                        analyzer = "standardAnalyzer")
176        @OptimisticLock(excluded = true)
177        @IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "myVersion")))
178        private String myNarrativeText;
179
180        @Transient
181        @IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "myVersion")))
182        @PropertyBinding(binder = @PropertyBinderRef(type = SearchParamTextPropertyBinder.class))
183        private ExtendedHSearchIndexData myLuceneIndexData;
184
185        @OneToMany(
186                        mappedBy = "myResource",
187                        cascade = {},
188                        fetch = FetchType.LAZY,
189                        orphanRemoval = false)
190        @OptimisticLock(excluded = true)
191        private Collection<ResourceIndexedSearchParamCoords> myParamsCoords;
192
193        @Column(name = "SP_COORDS_PRESENT", nullable = false)
194        @OptimisticLock(excluded = true)
195        private boolean myParamsCoordsPopulated;
196
197        @OneToMany(
198                        mappedBy = "myResource",
199                        cascade = {},
200                        fetch = FetchType.LAZY,
201                        orphanRemoval = false)
202        @OptimisticLock(excluded = true)
203        private Collection<ResourceIndexedSearchParamDate> myParamsDate;
204
205        @Column(name = "SP_DATE_PRESENT", nullable = false)
206        @OptimisticLock(excluded = true)
207        private boolean myParamsDatePopulated;
208
209        @OptimisticLock(excluded = true)
210        @OneToMany(
211                        mappedBy = "myResource",
212                        cascade = {},
213                        fetch = FetchType.LAZY,
214                        orphanRemoval = false)
215        private Collection<ResourceIndexedSearchParamNumber> myParamsNumber;
216
217        @Column(name = "SP_NUMBER_PRESENT", nullable = false)
218        @OptimisticLock(excluded = true)
219        private boolean myParamsNumberPopulated;
220
221        @OneToMany(
222                        mappedBy = "myResource",
223                        cascade = {},
224                        fetch = FetchType.LAZY,
225                        orphanRemoval = false)
226        @OptimisticLock(excluded = true)
227        private Collection<ResourceIndexedSearchParamQuantity> myParamsQuantity;
228
229        @Column(name = "SP_QUANTITY_PRESENT", nullable = false)
230        @OptimisticLock(excluded = true)
231        private boolean myParamsQuantityPopulated;
232
233        /**
234         * Added to support UCUM conversion
235         * since 5.3.0
236         */
237        @OneToMany(
238                        mappedBy = "myResource",
239                        cascade = {},
240                        fetch = FetchType.LAZY,
241                        orphanRemoval = false)
242        @OptimisticLock(excluded = true)
243        private Collection<ResourceIndexedSearchParamQuantityNormalized> myParamsQuantityNormalized;
244
245        /**
246         * Added to support UCUM conversion,
247         * NOTE : use Boolean class instead of boolean primitive, in order to set the existing rows to null
248         * since 5.3.0
249         */
250        @Column(name = "SP_QUANTITY_NRML_PRESENT", nullable = false)
251        @OptimisticLock(excluded = true)
252        private Boolean myParamsQuantityNormalizedPopulated = Boolean.FALSE;
253
254        @OneToMany(
255                        mappedBy = "myResource",
256                        cascade = {},
257                        fetch = FetchType.LAZY,
258                        orphanRemoval = false)
259        @OptimisticLock(excluded = true)
260        private Collection<ResourceIndexedSearchParamString> myParamsString;
261
262        @Column(name = "SP_STRING_PRESENT", nullable = false)
263        @OptimisticLock(excluded = true)
264        private boolean myParamsStringPopulated;
265
266        @OneToMany(
267                        mappedBy = "myResource",
268                        cascade = {},
269                        fetch = FetchType.LAZY,
270                        orphanRemoval = false)
271        @OptimisticLock(excluded = true)
272        private Collection<ResourceIndexedSearchParamToken> myParamsToken;
273
274        @Column(name = "SP_TOKEN_PRESENT", nullable = false)
275        @OptimisticLock(excluded = true)
276        private boolean myParamsTokenPopulated;
277
278        @OneToMany(
279                        mappedBy = "myResource",
280                        cascade = {},
281                        fetch = FetchType.LAZY,
282                        orphanRemoval = false)
283        @OptimisticLock(excluded = true)
284        private Collection<ResourceIndexedSearchParamUri> myParamsUri;
285
286        @Column(name = "SP_URI_PRESENT", nullable = false)
287        @OptimisticLock(excluded = true)
288        private boolean myParamsUriPopulated;
289
290        // Added in 3.0.0 - Should make this a primitive Boolean at some point
291        @OptimisticLock(excluded = true)
292        @Column(name = "SP_CMPSTR_UNIQ_PRESENT")
293        private Boolean myParamsComboStringUniquePresent = false;
294
295        @OneToMany(
296                        mappedBy = "myResource",
297                        cascade = {},
298                        fetch = FetchType.LAZY,
299                        orphanRemoval = false)
300        @OptimisticLock(excluded = true)
301        private Collection<ResourceIndexedComboStringUnique> myParamsComboStringUnique;
302
303        // Added in 5.5.0 - Should make this a primitive Boolean at some point
304        @OptimisticLock(excluded = true)
305        @Column(name = "SP_CMPTOKS_PRESENT")
306        private Boolean myParamsComboTokensNonUniquePresent = false;
307
308        @OneToMany(
309                        mappedBy = "myResource",
310                        cascade = {},
311                        fetch = FetchType.LAZY,
312                        orphanRemoval = false)
313        @OptimisticLock(excluded = true)
314        private Collection<ResourceIndexedComboTokenNonUnique> myParamsComboTokensNonUnique;
315
316        @OneToMany(
317                        mappedBy = "mySourceResource",
318                        cascade = {},
319                        fetch = FetchType.LAZY,
320                        orphanRemoval = false)
321        @OptimisticLock(excluded = true)
322        private Collection<ResourceLink> myResourceLinks;
323
324        /**
325         * This is a clone of {@link #myResourceLinks} but without the hibernate annotations.
326         * Before we persist we copy the contents of {@link #myResourceLinks} into this field. We
327         * have this separate because that way we can only populate this field if
328         * {@link #myHasLinks} is true, meaning that there are actually resource links present
329         * right now. This avoids Hibernate Search triggering a select on the resource link
330         * table.
331         * <p>
332         * This field is used by FulltextSearchSvcImpl
333         * <p>
334         * You can test that any changes don't cause extra queries by running
335         * FhirResourceDaoR4QueryCountTest
336         */
337        @FullTextField
338        @Transient
339        @IndexingDependency(derivedFrom = @ObjectPath(@PropertyValue(propertyName = "myResourceLinks")))
340        private String myResourceLinksField;
341
342        @OneToMany(
343                        mappedBy = "myTargetResource",
344                        cascade = {},
345                        fetch = FetchType.LAZY,
346                        orphanRemoval = false)
347        @OptimisticLock(excluded = true)
348        private Collection<ResourceLink> myResourceLinksAsTarget;
349
350        @Column(name = RES_TYPE, length = RESTYPE_LEN, nullable = false)
351        @FullTextField
352        @OptimisticLock(excluded = true)
353        private String myResourceType;
354
355        @OneToMany(mappedBy = "myResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
356        @OptimisticLock(excluded = true)
357        private Collection<SearchParamPresentEntity> mySearchParamPresents;
358
359        @OneToMany(mappedBy = "myResource", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
360        @OptimisticLock(excluded = true)
361        private Set<ResourceTag> myTags;
362
363        @Transient
364        private transient boolean myUnchangedInCurrentOperation;
365
366        /**
367         * The id of the Resource.
368         * Will contain either the client-assigned id, or the sequence value.
369         * Will be null during insert time until the first read.
370         */
371        @Column(
372                        name = FHIR_ID,
373                        // [A-Za-z0-9\-\.]{1,64} - https://www.hl7.org/fhir/datatypes.html#id
374                        length = 64,
375                        // we never update this after insert, and the Generator will otherwise "dirty" the object.
376                        updatable = false)
377
378        // inject the pk for server-assigned sequence ids.
379        @GeneratorType(when = GenerationTime.INSERT, type = FhirIdGenerator.class)
380        // Make sure the generator doesn't bump the history version.
381        @OptimisticLock(excluded = true)
382        private String myFhirId;
383
384        /**
385         * Is there a corresponding row in {@link ResourceSearchUrlEntity} for
386         * this row.
387         * TODO: Added in 6.6.0 - Should make this a primitive boolean at some point
388         */
389        @OptimisticLock(excluded = true)
390        @Column(name = "SEARCH_URL_PRESENT", nullable = true)
391        private Boolean mySearchUrlPresent = false;
392
393        @Version
394        @Column(name = "RES_VER", nullable = false)
395        private long myVersion;
396
397        @OneToMany(mappedBy = "myResourceTable", fetch = FetchType.LAZY)
398        private Collection<ResourceHistoryProvenanceEntity> myProvenance;
399
400        @Transient
401        private transient ResourceHistoryTable myCurrentVersionEntity;
402
403        @Transient
404        private transient boolean myVersionUpdatedInCurrentTransaction;
405
406        @Transient
407        private volatile String myCreatedByMatchUrl;
408
409        @Transient
410        private volatile String myUpdatedByMatchUrl;
411
412        /**
413         * Constructor
414         */
415        public ResourceTable() {
416                super();
417        }
418
419        /**
420         * Setting this flag is an indication that we're making changes and the version number will
421         * be incremented in the current transaction. When this is set, calls to {@link #getVersion()}
422         * will be incremented by one.
423         * This flag is cleared in {@link #postPersist()} since at that time the new version number
424         * should be reflected.
425         */
426        public void markVersionUpdatedInCurrentTransaction() {
427                if (!myVersionUpdatedInCurrentTransaction) {
428                        /*
429                         * Note that modifying this number doesn't actually directly affect what
430                         * gets stored in the database since this is a @Version field and the
431                         * value is therefore managed by Hibernate. So in other words, if the
432                         * row in the database is updated, it doesn't matter what we set
433                         * this field to, hibernate will increment it by one. However, we still
434                         * increment it for two reasons:
435                         * 1. The value gets used for the version attribute in the ResourceHistoryTable
436                         *    entity we create for each new version.
437                         * 2. For updates to existing resources, there may actually not be any other
438                         *    changes to this entity so incrementing this is a signal to
439                         *    Hibernate that something changed and we need to force an entity
440                         *    update.
441                         */
442                        myVersion++;
443                        this.myVersionUpdatedInCurrentTransaction = true;
444                }
445        }
446
447        @PostPersist
448        public void postPersist() {
449                myVersionUpdatedInCurrentTransaction = false;
450        }
451
452        @Override
453        public ResourceTag addTag(TagDefinition theTag) {
454                for (ResourceTag next : getTags()) {
455                        if (next.getTag().equals(theTag)) {
456                                return next;
457                        }
458                }
459                ResourceTag tag = new ResourceTag(this, theTag, getPartitionId());
460                getTags().add(tag);
461                return tag;
462        }
463
464        public String getHashSha256() {
465                return myHashSha256;
466        }
467
468        public void setHashSha256(String theHashSha256) {
469                myHashSha256 = theHashSha256;
470        }
471
472        @Nonnull
473        @Override
474        public JpaPid getId() {
475                if (myPid == null) {
476                        myPid = new JpaPid();
477                }
478                myPid.setVersion(myVersion);
479                myPid.setPartitionIdIfNotAlreadySet(myPartitionIdValue);
480                myPid.setResourceType(myResourceType);
481                return myPid;
482        }
483
484        public void setId(JpaPid thePid) {
485                myPid = thePid;
486        }
487
488        @VisibleForTesting
489        public void setIdForUnitTest(Long theId) {
490                setId(JpaPid.fromId(theId));
491        }
492
493        public EntityIndexStatusEnum getIndexStatus() {
494                return myIndexStatus;
495        }
496
497        public void setIndexStatus(EntityIndexStatusEnum theIndexStatus) {
498                myIndexStatus = theIndexStatus;
499        }
500
501        public Collection<ResourceIndexedComboStringUnique> getParamsComboStringUnique() {
502                if (myParamsComboStringUnique == null) {
503                        myParamsComboStringUnique = new ArrayList<>();
504                }
505                return myParamsComboStringUnique;
506        }
507
508        public Collection<ResourceIndexedComboTokenNonUnique> getmyParamsComboTokensNonUnique() {
509                if (myParamsComboTokensNonUnique == null) {
510                        myParamsComboTokensNonUnique = new ArrayList<>();
511                }
512                return myParamsComboTokensNonUnique;
513        }
514
515        public Collection<ResourceIndexedSearchParamCoords> getParamsCoords() {
516                if (myParamsCoords == null) {
517                        myParamsCoords = new ArrayList<>();
518                }
519                return myParamsCoords;
520        }
521
522        public void setParamsCoords(Collection<ResourceIndexedSearchParamCoords> theParamsCoords) {
523                if (!isParamsTokenPopulated() && theParamsCoords.isEmpty()) {
524                        return;
525                }
526                getParamsCoords().clear();
527                getParamsCoords().addAll(theParamsCoords);
528        }
529
530        public Collection<ResourceIndexedSearchParamDate> getParamsDate() {
531                if (myParamsDate == null) {
532                        myParamsDate = new ArrayList<>();
533                }
534                return myParamsDate;
535        }
536
537        public void setParamsDate(Collection<ResourceIndexedSearchParamDate> theParamsDate) {
538                if (!isParamsDatePopulated() && theParamsDate.isEmpty()) {
539                        return;
540                }
541                getParamsDate().clear();
542                getParamsDate().addAll(theParamsDate);
543        }
544
545        public Collection<ResourceIndexedSearchParamNumber> getParamsNumber() {
546                if (myParamsNumber == null) {
547                        myParamsNumber = new ArrayList<>();
548                }
549                return myParamsNumber;
550        }
551
552        public void setParamsNumber(Collection<ResourceIndexedSearchParamNumber> theNumberParams) {
553                if (!isParamsNumberPopulated() && theNumberParams.isEmpty()) {
554                        return;
555                }
556                getParamsNumber().clear();
557                getParamsNumber().addAll(theNumberParams);
558        }
559
560        public Collection<ResourceIndexedSearchParamQuantity> getParamsQuantity() {
561                if (myParamsQuantity == null) {
562                        myParamsQuantity = new ArrayList<>();
563                }
564                return myParamsQuantity;
565        }
566
567        public void setParamsQuantity(Collection<ResourceIndexedSearchParamQuantity> theQuantityParams) {
568                if (!isParamsQuantityPopulated() && theQuantityParams.isEmpty()) {
569                        return;
570                }
571                getParamsQuantity().clear();
572                getParamsQuantity().addAll(theQuantityParams);
573        }
574
575        public Collection<ResourceIndexedSearchParamQuantityNormalized> getParamsQuantityNormalized() {
576                if (myParamsQuantityNormalized == null) {
577                        myParamsQuantityNormalized = new ArrayList<>();
578                }
579                return myParamsQuantityNormalized;
580        }
581
582        public void setParamsQuantityNormalized(
583                        Collection<ResourceIndexedSearchParamQuantityNormalized> theQuantityNormalizedParams) {
584                if (!isParamsQuantityNormalizedPopulated() && theQuantityNormalizedParams.isEmpty()) {
585                        return;
586                }
587                getParamsQuantityNormalized().clear();
588                getParamsQuantityNormalized().addAll(theQuantityNormalizedParams);
589        }
590
591        public Collection<ResourceIndexedSearchParamString> getParamsString() {
592                if (myParamsString == null) {
593                        myParamsString = new ArrayList<>();
594                }
595                return myParamsString;
596        }
597
598        public void setParamsString(Collection<ResourceIndexedSearchParamString> theParamsString) {
599                if (!isParamsStringPopulated() && theParamsString.isEmpty()) {
600                        return;
601                }
602                getParamsString().clear();
603                getParamsString().addAll(theParamsString);
604        }
605
606        public Collection<ResourceIndexedSearchParamToken> getParamsToken() {
607                if (myParamsToken == null) {
608                        myParamsToken = new ArrayList<>();
609                }
610                return myParamsToken;
611        }
612
613        public void setParamsToken(Collection<ResourceIndexedSearchParamToken> theParamsToken) {
614                if (!isParamsTokenPopulated() && theParamsToken.isEmpty()) {
615                        return;
616                }
617                getParamsToken().clear();
618                getParamsToken().addAll(theParamsToken);
619        }
620
621        public Collection<ResourceIndexedSearchParamUri> getParamsUri() {
622                if (myParamsUri == null) {
623                        myParamsUri = new ArrayList<>();
624                }
625                return myParamsUri;
626        }
627
628        public void setParamsUri(Collection<ResourceIndexedSearchParamUri> theParamsUri) {
629                if (!isParamsTokenPopulated() && theParamsUri.isEmpty()) {
630                        return;
631                }
632                getParamsUri().clear();
633                getParamsUri().addAll(theParamsUri);
634        }
635
636        @Override
637        public JpaPid getResourceId() {
638                return getId();
639        }
640
641        public Collection<ResourceLink> getResourceLinks() {
642                if (myResourceLinks == null) {
643                        myResourceLinks = new ArrayList<>();
644                }
645                return myResourceLinks;
646        }
647
648        public void setResourceLinks(Collection<ResourceLink> theLinks) {
649                if (!isHasLinks() && theLinks.isEmpty()) {
650                        return;
651                }
652                getResourceLinks().clear();
653                getResourceLinks().addAll(theLinks);
654        }
655
656        @Override
657        public String getResourceType() {
658                return myResourceType;
659        }
660
661        public ResourceTable setResourceType(String theResourceType) {
662                myResourceType = theResourceType;
663                return this;
664        }
665
666        @Override
667        public Collection<ResourceTag> getTags() {
668                if (myTags == null) {
669                        myTags = new HashSet<>();
670                }
671                return myTags;
672        }
673
674        @Override
675        public long getVersion() {
676                return myVersion;
677        }
678
679        @Nonnull
680        @Override
681        public PartitionablePartitionId getPartitionId() {
682                PartitionablePartitionId retVal = getId().getPartitionablePartitionId();
683                if (myPartitionIdValue != null) {
684                        retVal.setPartitionId(myPartitionIdValue);
685                }
686                if (myPartitionDateValue != null) {
687                        retVal.setPartitionDate(myPartitionDateValue);
688                }
689                return retVal;
690        }
691
692        /**
693         * Sets the version on this entity to {@literal 1}. This should only be called
694         * on resources that are not yet persisted. After that time the version number
695         * is managed by hibernate.
696         */
697        public void initializeVersion() {
698                assert myPid == null || myPid.getId() == null;
699                myVersion = 1;
700        }
701
702        /**
703         * Don't call this in any JPA environments, the version will be ignored
704         * since this field is managed by hibernate
705         */
706        @VisibleForTesting
707        public void setVersionForUnitTest(long theVersion) {
708                myVersion = theVersion;
709        }
710
711        @Override
712        public boolean isDeleted() {
713                return getDeleted() != null;
714        }
715
716        @Override
717        public void setNotDeleted() {
718                setDeleted(null);
719        }
720
721        public boolean isHasLinks() {
722                return myHasLinks;
723        }
724
725        public void setHasLinks(boolean theHasLinks) {
726                myHasLinks = theHasLinks;
727        }
728
729        /**
730         * Clears all the index population flags, e.g. {@link #isParamsStringPopulated()}
731         *
732         * @since 6.8.0
733         */
734        public void clearAllParamsPopulated() {
735                myParamsTokenPopulated = false;
736                myParamsCoordsPopulated = false;
737                myParamsDatePopulated = false;
738                myParamsNumberPopulated = false;
739                myParamsStringPopulated = false;
740                myParamsQuantityPopulated = false;
741                myParamsQuantityNormalizedPopulated = false;
742                myParamsUriPopulated = false;
743                myHasLinks = false;
744        }
745
746        public boolean isParamsComboStringUniquePresent() {
747                if (myParamsComboStringUniquePresent == null) {
748                        return false;
749                }
750                return myParamsComboStringUniquePresent;
751        }
752
753        public void setParamsComboStringUniquePresent(boolean theParamsComboStringUniquePresent) {
754                myParamsComboStringUniquePresent = theParamsComboStringUniquePresent;
755        }
756
757        public boolean isParamsComboTokensNonUniquePresent() {
758                if (myParamsComboTokensNonUniquePresent == null) {
759                        return false;
760                }
761                return myParamsComboTokensNonUniquePresent;
762        }
763
764        public void setParamsComboTokensNonUniquePresent(boolean theParamsComboTokensNonUniquePresent) {
765                myParamsComboTokensNonUniquePresent = theParamsComboTokensNonUniquePresent;
766        }
767
768        public boolean isParamsCoordsPopulated() {
769                return myParamsCoordsPopulated;
770        }
771
772        public void setParamsCoordsPopulated(boolean theParamsCoordsPopulated) {
773                myParamsCoordsPopulated = theParamsCoordsPopulated;
774        }
775
776        public boolean isParamsDatePopulated() {
777                return myParamsDatePopulated;
778        }
779
780        public void setParamsDatePopulated(boolean theParamsDatePopulated) {
781                myParamsDatePopulated = theParamsDatePopulated;
782        }
783
784        public boolean isParamsNumberPopulated() {
785                return myParamsNumberPopulated;
786        }
787
788        public void setParamsNumberPopulated(boolean theParamsNumberPopulated) {
789                myParamsNumberPopulated = theParamsNumberPopulated;
790        }
791
792        public boolean isParamsQuantityPopulated() {
793                return myParamsQuantityPopulated;
794        }
795
796        public void setParamsQuantityPopulated(boolean theParamsQuantityPopulated) {
797                myParamsQuantityPopulated = theParamsQuantityPopulated;
798        }
799
800        public Boolean isParamsQuantityNormalizedPopulated() {
801                if (myParamsQuantityNormalizedPopulated == null) return Boolean.FALSE;
802                else return myParamsQuantityNormalizedPopulated;
803        }
804
805        public void setParamsQuantityNormalizedPopulated(Boolean theParamsQuantityNormalizedPopulated) {
806                if (theParamsQuantityNormalizedPopulated == null) myParamsQuantityNormalizedPopulated = Boolean.FALSE;
807                else myParamsQuantityNormalizedPopulated = theParamsQuantityNormalizedPopulated;
808        }
809
810        public boolean isParamsStringPopulated() {
811                return myParamsStringPopulated;
812        }
813
814        public void setParamsStringPopulated(boolean theParamsStringPopulated) {
815                myParamsStringPopulated = theParamsStringPopulated;
816        }
817
818        public boolean isParamsTokenPopulated() {
819                return myParamsTokenPopulated;
820        }
821
822        public void setParamsTokenPopulated(boolean theParamsTokenPopulated) {
823                myParamsTokenPopulated = theParamsTokenPopulated;
824        }
825
826        public boolean isParamsUriPopulated() {
827                return myParamsUriPopulated;
828        }
829
830        public void setParamsUriPopulated(boolean theParamsUriPopulated) {
831                myParamsUriPopulated = theParamsUriPopulated;
832        }
833
834        /**
835         * Transient (not saved in DB) flag indicating that this resource was found to be unchanged by the current operation
836         * and was not re-saved in the database
837         */
838        public boolean isUnchangedInCurrentOperation() {
839                return myUnchangedInCurrentOperation;
840        }
841
842        /**
843         * Transient (not saved in DB) flag indicating that this resource was found to be unchanged by the current operation
844         * and was not re-saved in the database
845         */
846        public void setUnchangedInCurrentOperation(boolean theUnchangedInCurrentOperation) {
847
848                myUnchangedInCurrentOperation = theUnchangedInCurrentOperation;
849        }
850
851        public String getContentText() {
852                return myContentText;
853        }
854
855        public void setContentText(String theContentText) {
856                myContentText = theContentText;
857        }
858
859        public void setNarrativeText(String theNarrativeText) {
860                myNarrativeText = theNarrativeText;
861        }
862
863        public boolean isSearchUrlPresent() {
864                return Boolean.TRUE.equals(mySearchUrlPresent);
865        }
866
867        public void setSearchUrlPresent(boolean theSearchUrlPresent) {
868                mySearchUrlPresent = theSearchUrlPresent;
869        }
870
871        /**
872         * This method creates a new history entity, or might reuse the current one if we've
873         * already created one in the current transaction. This is because we can only increment
874         * the version once in a DB transaction (since hibernate manages that number) so creating
875         * multiple {@link ResourceHistoryTable} entities will result in a constraint error.
876         */
877        public ResourceHistoryTable toHistory(boolean theCreateVersionTags) {
878
879                ResourceHistoryTable retVal = new ResourceHistoryTable();
880
881                retVal.setResourceId(myPid.getId());
882                retVal.setResourceType(myResourceType);
883                retVal.setTransientForcedId(getFhirId());
884                retVal.setFhirVersion(getFhirVersion());
885                retVal.setResourceTable(this);
886                retVal.setPartitionId(getPartitionId());
887
888                retVal.setHasTags(isHasTags());
889                if (isHasTags() && theCreateVersionTags) {
890                        for (ResourceTag next : getTags()) {
891                                retVal.addTag(next);
892                        }
893                }
894
895                // If we've deleted and updated the same resource in the same transaction,
896                // we need to actually create 2 distinct versions
897                if (getCurrentVersionEntity() != null
898                                && getCurrentVersionEntity().getId() != null
899                                && getVersion() == getCurrentVersionEntity().getVersion()) {
900                        myVersion++;
901                }
902
903                populateHistoryEntityVersionAndDates(retVal);
904
905                return retVal;
906        }
907
908        /**
909         * Updates several temporal values in a {@link ResourceHistoryTable} entity which
910         * are pulled from this entity, including the resource version, and the
911         * creation, update, and deletion dates.
912         */
913        public void populateHistoryEntityVersionAndDates(ResourceHistoryTable theResourceHistoryTable) {
914                theResourceHistoryTable.setVersion(getVersion());
915                theResourceHistoryTable.setPublished(getPublishedDate());
916                theResourceHistoryTable.setUpdated(getUpdatedDate());
917                theResourceHistoryTable.setDeleted(getDeleted());
918        }
919
920        @Override
921        public String toString() {
922                ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
923                b.append("partition", getPartitionId().getPartitionId());
924                b.append("pid", getId().getId());
925                b.append("fhirId", myFhirId);
926                b.append("resourceType", myResourceType);
927                b.append("version", myVersion);
928                b.append("lastUpdated", getUpdated().getValueAsString());
929                if (getDeleted() != null) {
930                        b.append("deleted", new InstantType(getDeleted()).getValueAsString());
931                }
932                return b.build();
933        }
934
935        @PrePersist
936        @PreUpdate
937        public void preSave() {
938                if (myHasLinks && myResourceLinks != null) {
939                        myResourceLinksField = getResourceLinks().stream()
940                                        .map(ResourceLink::getTargetResourcePid)
941                                        .filter(Objects::nonNull)
942                                        .map(Object::toString)
943                                        .collect(Collectors.joining(" "));
944                } else {
945                        myResourceLinksField = null;
946                }
947        }
948
949        /**
950         * This is a convenience to avoid loading the version a second time within a single transaction. It is
951         * not persisted.
952         */
953        public ResourceHistoryTable getCurrentVersionEntity() {
954                return myCurrentVersionEntity;
955        }
956
957        /**
958         * This is a convenience to avoid loading the version a second time within a single transaction. It is
959         * not persisted.
960         */
961        public void setCurrentVersionEntity(ResourceHistoryTable theCurrentVersionEntity) {
962                myCurrentVersionEntity = theCurrentVersionEntity;
963        }
964
965        @Override
966        public JpaPid getPersistentId() {
967                return getId();
968        }
969
970        @Override
971        public IdDt getIdDt() {
972                IdDt retVal = new IdDt();
973                populateId(retVal);
974                return retVal;
975        }
976
977        public IIdType getIdType(FhirContext theContext) {
978                IIdType retVal = theContext.getVersion().newIdType();
979                populateId(retVal);
980                return retVal;
981        }
982
983        private void populateId(IIdType retVal) {
984                String resourceId;
985                if (myFhirId != null && !myFhirId.isEmpty()) {
986                        resourceId = myFhirId;
987                } else {
988                        Long id = getResourceId().getId();
989                        resourceId = Long.toString(id);
990                }
991                retVal.setValue(getResourceType() + '/' + resourceId + '/' + Constants.PARAM_HISTORY + '/' + getVersion());
992        }
993
994        public String getCreatedByMatchUrl() {
995                return myCreatedByMatchUrl;
996        }
997
998        public void setCreatedByMatchUrl(String theCreatedByMatchUrl) {
999                myCreatedByMatchUrl = theCreatedByMatchUrl;
1000        }
1001
1002        public String getUpdatedByMatchUrl() {
1003                return myUpdatedByMatchUrl;
1004        }
1005
1006        public void setUpdatedByMatchUrl(String theUpdatedByMatchUrl) {
1007                myUpdatedByMatchUrl = theUpdatedByMatchUrl;
1008        }
1009
1010        public boolean isVersionUpdatedInCurrentTransaction() {
1011                return myVersionUpdatedInCurrentTransaction;
1012        }
1013
1014        public void setLuceneIndexData(ExtendedHSearchIndexData theLuceneIndexData) {
1015                myLuceneIndexData = theLuceneIndexData;
1016        }
1017
1018        public Collection<SearchParamPresentEntity> getSearchParamPresents() {
1019                if (mySearchParamPresents == null) {
1020                        mySearchParamPresents = new ArrayList<>();
1021                }
1022                return mySearchParamPresents;
1023        }
1024
1025        /**
1026         * Get the FHIR resource id.
1027         *
1028         * @return the resource id, or null if the resource doesn't have a client-assigned id,
1029         * and hasn't been saved to the db to get a server-assigned id yet.
1030         */
1031        @Override
1032        public String getFhirId() {
1033                return myFhirId;
1034        }
1035
1036        public void setFhirId(String theFhirId) {
1037                myFhirId = theFhirId;
1038        }
1039
1040        public String asTypedFhirResourceId() {
1041                return getResourceType() + "/" + getFhirId();
1042        }
1043
1044        public void setPartitionId(PartitionablePartitionId theStoragePartition) {
1045                if (myPid == null) {
1046                        myPid = new JpaPid();
1047                }
1048                myPid.setPartitionId(theStoragePartition.getPartitionId());
1049                myPartitionIdValue = theStoragePartition.getPartitionId();
1050                myPartitionDateValue = theStoragePartition.getPartitionDate();
1051        }
1052
1053        public void setParamsComboStringUnique(Collection<ResourceIndexedComboStringUnique> theComboStringUniques) {
1054                myParamsComboStringUnique = theComboStringUniques;
1055        }
1056
1057        public void setParamsComboTokensNonUnique(Collection<ResourceIndexedComboTokenNonUnique> theComboTokensNonUnique) {
1058                myParamsComboTokensNonUnique = theComboTokensNonUnique;
1059        }
1060
1061        /**
1062         * Populate myFhirId with server-assigned sequence id when no client-id provided.
1063         * We eat this complexity during insert to simplify query time with a uniform column.
1064         * Server-assigned sequence ids aren't available until just before insertion.
1065         * Hibernate calls insert Generators after the pk has been assigned, so we can use myId safely here.
1066         */
1067        public static final class FhirIdGenerator implements ValueGenerator<String> {
1068                @Override
1069                public String generateValue(Session session, Object owner) {
1070                        ResourceTable that = (ResourceTable) owner;
1071                        return that.myFhirId != null ? that.myFhirId : that.myPid.getId().toString();
1072                }
1073        }
1074}