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