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