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