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