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