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.jpa.model.dao.JpaPid;
023import jakarta.persistence.Column;
024import jakarta.persistence.ConstraintMode;
025import jakarta.persistence.Entity;
026import jakarta.persistence.FetchType;
027import jakarta.persistence.ForeignKey;
028import jakarta.persistence.GeneratedValue;
029import jakarta.persistence.GenerationType;
030import jakarta.persistence.Id;
031import jakarta.persistence.IdClass;
032import jakarta.persistence.Index;
033import jakarta.persistence.JoinColumn;
034import jakarta.persistence.JoinColumns;
035import jakarta.persistence.ManyToOne;
036import jakarta.persistence.PostLoad;
037import jakarta.persistence.Table;
038import jakarta.persistence.Temporal;
039import jakarta.persistence.TemporalType;
040import jakarta.persistence.Transient;
041import org.apache.commons.lang3.Validate;
042import org.apache.commons.lang3.builder.EqualsBuilder;
043import org.apache.commons.lang3.builder.HashCodeBuilder;
044import org.hibernate.annotations.GenericGenerator;
045import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
046import org.hl7.fhir.instance.model.api.IIdType;
047
048import java.time.LocalDate;
049import java.util.Date;
050
051@Entity
052@Table(
053                name = "HFJ_RES_LINK",
054                indexes = {
055                        // We need to join both ways, so index from src->tgt and from tgt->src.
056                        // From src->tgt, rows are usually written all together as part of ingestion - keep the index small, and
057                        // read blocks as needed.
058                        @Index(name = "IDX_RL_SRC", columnList = "SRC_RESOURCE_ID"),
059                        // But from tgt->src, include all the match columns. Targets will usually be randomly distributed - each row
060                        // in separate block.
061                        @Index(
062                                        name = "IDX_RL_TGT_v2",
063                                        columnList = "TARGET_RESOURCE_ID, SRC_PATH, SRC_RESOURCE_ID, TARGET_RESOURCE_TYPE,PARTITION_ID")
064                })
065@IdClass(IdAndPartitionId.class)
066public class ResourceLink extends BaseResourceIndex {
067
068        public static final int SRC_PATH_LENGTH = 500;
069        private static final long serialVersionUID = 1L;
070        public static final String TARGET_RES_PARTITION_ID = "TARGET_RES_PARTITION_ID";
071        public static final String TARGET_RESOURCE_ID = "TARGET_RESOURCE_ID";
072        public static final String FK_RESLINK_TARGET = "FK_RESLINK_TARGET";
073
074        @GenericGenerator(name = "SEQ_RESLINK_ID", type = ca.uhn.fhir.jpa.model.dialect.HapiSequenceStyleGenerator.class)
075        @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RESLINK_ID")
076        @Id
077        @Column(name = "PID")
078        private Long myId;
079
080        @Column(name = "SRC_PATH", length = SRC_PATH_LENGTH, nullable = false)
081        private String mySourcePath;
082
083        @ManyToOne(
084                        optional = false,
085                        fetch = FetchType.LAZY,
086                        cascade = {})
087        @JoinColumns(
088                        value = {
089                                @JoinColumn(
090                                                name = "SRC_RESOURCE_ID",
091                                                referencedColumnName = "RES_ID",
092                                                insertable = false,
093                                                updatable = false,
094                                                nullable = false),
095                                @JoinColumn(
096                                                name = "PARTITION_ID",
097                                                referencedColumnName = "PARTITION_ID",
098                                                insertable = false,
099                                                updatable = false,
100                                                nullable = false)
101                        },
102                        foreignKey = @ForeignKey(name = "FK_RESLINK_SOURCE"))
103        private ResourceTable mySourceResource;
104
105        @Column(name = "SRC_RESOURCE_ID", nullable = false)
106        private Long mySourceResourcePid;
107
108        @Column(name = "SOURCE_RESOURCE_TYPE", updatable = false, nullable = false, length = ResourceTable.RESTYPE_LEN)
109        @FullTextField
110        private String mySourceResourceType;
111
112        @ManyToOne(optional = true, fetch = FetchType.EAGER)
113        @JoinColumns(
114                        value = {
115                                @JoinColumn(
116                                                name = TARGET_RESOURCE_ID,
117                                                referencedColumnName = "RES_ID",
118                                                nullable = true,
119                                                insertable = false,
120                                                updatable = false),
121                                @JoinColumn(
122                                                name = TARGET_RES_PARTITION_ID,
123                                                referencedColumnName = "PARTITION_ID",
124                                                nullable = true,
125                                                insertable = false,
126                                                updatable = false),
127                        },
128                        /*
129                         * TODO: We need to drop this constraint because it affects performance in pretty
130                         *  terrible ways on a lot of platforms. But a Hibernate bug present in Hibernate 6.6.4
131                         *  makes it impossible.
132                         *  See: https://hibernate.atlassian.net/browse/HHH-19046
133                         */
134                        foreignKey = @ForeignKey(name = FK_RESLINK_TARGET))
135        private ResourceTable myTargetResource;
136
137        @Transient
138        private ResourceTable myTransientTargetResource;
139
140        @Column(name = TARGET_RESOURCE_ID, insertable = true, updatable = true, nullable = true)
141        @FullTextField
142        private Long myTargetResourcePid;
143
144        @Column(name = "TARGET_RESOURCE_TYPE", nullable = false, length = ResourceTable.RESTYPE_LEN)
145        @FullTextField
146        private String myTargetResourceType;
147
148        @Column(name = "TARGET_RESOURCE_URL", length = 200, nullable = true)
149        @FullTextField
150        private String myTargetResourceUrl;
151
152        @Column(name = "TARGET_RESOURCE_VERSION", nullable = true)
153        private Long myTargetResourceVersion;
154
155        @FullTextField
156        @Column(name = "SP_UPDATED", nullable = true) // TODO: make this false after HAPI 2.3
157        @Temporal(TemporalType.TIMESTAMP)
158        private Date myUpdated;
159
160        @Transient
161        private transient String myTargetResourceId;
162
163        @Column(name = TARGET_RES_PARTITION_ID, nullable = true)
164        private Integer myTargetResourcePartitionId;
165
166        @Column(name = "TARGET_RES_PARTITION_DATE", nullable = true)
167        private LocalDate myTargetResourcePartitionDate;
168
169        @ManyToOne(fetch = FetchType.LAZY)
170        @JoinColumn(
171                        name = "SRC_RES_TYPE_ID",
172                        referencedColumnName = "RES_TYPE_ID",
173                        foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT),
174                        insertable = false,
175                        updatable = false,
176                        nullable = true)
177        private ResourceTypeEntity mySourceResTypeEntity;
178
179        @Column(name = "SRC_RES_TYPE_ID", nullable = true)
180        private Short mySourceResourceTypeId;
181
182        @ManyToOne(fetch = FetchType.LAZY)
183        @JoinColumn(
184                        name = "TARGET_RES_TYPE_ID",
185                        referencedColumnName = "RES_TYPE_ID",
186                        foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT),
187                        insertable = false,
188                        updatable = false,
189                        nullable = true)
190        private ResourceTypeEntity myTargetResTypeEntity;
191
192        @Column(name = "TARGET_RES_TYPE_ID", nullable = true)
193        private Short myTargetResourceTypeId;
194        /**
195         * Constructor
196         */
197        public ResourceLink() {
198                super();
199        }
200
201        public Long getTargetResourceVersion() {
202                return myTargetResourceVersion;
203        }
204
205        public void setTargetResourceVersion(Long theTargetResourceVersion) {
206                myTargetResourceVersion = theTargetResourceVersion;
207        }
208
209        public String getTargetResourceId() {
210                if (myTargetResourceId == null && getTargetResource() != null) {
211                        myTargetResourceId = getTargetResource().getIdDt().getIdPart();
212                }
213                return myTargetResourceId;
214        }
215
216        public String getSourceResourceType() {
217                return mySourceResourceType;
218        }
219
220        public String getTargetResourceType() {
221                return myTargetResourceType;
222        }
223
224        @Override
225        public boolean equals(Object theObj) {
226                if (this == theObj) {
227                        return true;
228                }
229                if (theObj == null) {
230                        return false;
231                }
232                if (!(theObj instanceof ResourceLink)) {
233                        return false;
234                }
235                ResourceLink obj = (ResourceLink) theObj;
236                EqualsBuilder b = new EqualsBuilder();
237                b.append(mySourcePath, obj.mySourcePath);
238                b.append(mySourceResource, obj.mySourceResource);
239                b.append(myTargetResourceUrl, obj.myTargetResourceUrl);
240                b.append(myTargetResourceType, obj.myTargetResourceType);
241                b.append(myTargetResourceVersion, obj.myTargetResourceVersion);
242                // In cases where we are extracting links from a resource that has not yet been persisted, the target resource
243                // pid
244                // will be null so we use the target resource id to differentiate instead
245                if (getTargetResourcePid() == null) {
246                        b.append(getTargetResourceId(), obj.getTargetResourceId());
247                } else {
248                        b.append(getTargetResourcePid(), obj.getTargetResourcePid());
249                }
250                return b.isEquals();
251        }
252
253        /**
254         * ResourceLink.myTargetResource field is immutable.Transient ResourceLink.myTransientTargetResource property
255         * is used instead, allowing it to be updated via the ResourceLink#copyMutableValuesFrom method
256         * when ResourceLink table row is reused.
257         */
258        @PostLoad
259        public void postLoad() {
260                myTransientTargetResource = myTargetResource;
261        }
262
263        @Override
264        public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) {
265                ResourceLink source = (ResourceLink) theSource;
266                mySourcePath = source.getSourcePath();
267                myTransientTargetResource = source.getTargetResource();
268                myTargetResourceId = source.getTargetResourceId();
269                myTargetResourcePid = source.getTargetResourcePid();
270                myTargetResourceType = source.getTargetResourceType();
271                myTargetResourceTypeId = source.getTargetResourceTypeId();
272                myTargetResourceVersion = source.getTargetResourceVersion();
273                myTargetResourceUrl = source.getTargetResourceUrl();
274                myTargetResourcePartitionId = source.getTargetResourcePartitionId();
275                myTargetResourcePartitionDate = source.getTargetResourcePartitionDate();
276        }
277
278        @Override
279        public void setResourceId(Long theResourceId) {
280                mySourceResourcePid = theResourceId;
281        }
282
283        public String getSourcePath() {
284                return mySourcePath;
285        }
286
287        public void setSourcePath(String theSourcePath) {
288                mySourcePath = theSourcePath;
289        }
290
291        public JpaPid getSourceResourcePk() {
292                return JpaPid.fromId(mySourceResourcePid, myPartitionIdValue);
293        }
294
295        public ResourceTable getSourceResource() {
296                return mySourceResource;
297        }
298
299        public void setSourceResource(ResourceTable theSourceResource) {
300                mySourceResource = theSourceResource;
301                mySourceResourcePid = theSourceResource.getId().getId();
302                mySourceResourceType = theSourceResource.getResourceType();
303                mySourceResourceTypeId = theSourceResource.getResourceTypeId();
304                setPartitionId(theSourceResource.getPartitionId());
305        }
306
307        public void setTargetResource(String theResourceType, Long theResourcePid, String theTargetResourceId) {
308                Validate.notBlank(theResourceType);
309
310                myTargetResourceType = theResourceType;
311                myTargetResourcePid = theResourcePid;
312                myTargetResourceId = theTargetResourceId;
313        }
314
315        public String getTargetResourceUrl() {
316                return myTargetResourceUrl;
317        }
318
319        public void setTargetResourceUrl(IIdType theTargetResourceUrl) {
320                Validate.isTrue(theTargetResourceUrl.hasBaseUrl());
321                Validate.isTrue(theTargetResourceUrl.hasResourceType());
322
323                //              if (theTargetResourceUrl.hasIdPart()) {
324                // do nothing
325                //              } else {
326                // Must have set an url like http://example.org/something
327                // We treat 'something' as the resource type because of fix for #659. Prior to #659 fix, 'something' was
328                // treated as the id and 'example.org' was treated as the resource type
329                // Maybe log a warning?
330                //              }
331
332                myTargetResourceType = theTargetResourceUrl.getResourceType();
333                myTargetResourceUrl = theTargetResourceUrl.getValue();
334        }
335
336        public Long getTargetResourcePid() {
337                return myTargetResourcePid;
338        }
339
340        public void setTargetResourceUrlCanonical(String theTargetResourceUrl) {
341                Validate.notBlank(theTargetResourceUrl);
342
343                myTargetResourceType = "(unknown)";
344                myTargetResourceUrl = theTargetResourceUrl;
345        }
346
347        public Date getUpdated() {
348                return myUpdated;
349        }
350
351        public void setUpdated(Date theUpdated) {
352                myUpdated = theUpdated;
353        }
354
355        @Override
356        public Long getId() {
357                return myId;
358        }
359
360        @Override
361        public void setId(Long theId) {
362                myId = theId;
363        }
364
365        public LocalDate getTargetResourcePartitionDate() {
366                return myTargetResourcePartitionDate;
367        }
368
369        public Integer getTargetResourcePartitionId() {
370                return myTargetResourcePartitionId;
371        }
372
373        public ResourceLink setTargetResourcePartitionId(PartitionablePartitionId theTargetResourcePartitionId) {
374                myTargetResourcePartitionId = null;
375                myTargetResourcePartitionDate = null;
376                if (theTargetResourcePartitionId != null) {
377                        myTargetResourcePartitionId = theTargetResourcePartitionId.getPartitionId();
378                        myTargetResourcePartitionDate = theTargetResourcePartitionId.getPartitionDate();
379                }
380                return this;
381        }
382
383        public Short getSourceResourceTypeId() {
384                return mySourceResourceTypeId;
385        }
386
387        public ResourceTypeEntity getSourceResTypeEntity() {
388                return mySourceResTypeEntity;
389        }
390
391        public Short getTargetResourceTypeId() {
392                return myTargetResourceTypeId;
393        }
394
395        public void setTargetResourceTypeId(Short theTargetResourceTypeId) {
396                myTargetResourceTypeId = theTargetResourceTypeId;
397        }
398
399        public ResourceTypeEntity getTargetResTypeEntity() {
400                return myTargetResTypeEntity;
401        }
402
403        @Override
404        public void clearHashes() {
405                // nothing right now
406        }
407
408        @Override
409        public void calculateHashes() {
410                // nothing right now
411        }
412
413        @Override
414        public int hashCode() {
415                HashCodeBuilder b = new HashCodeBuilder();
416                b.append(mySourcePath);
417                b.append(mySourceResource);
418                b.append(myTargetResourceUrl);
419                b.append(myTargetResourceVersion);
420
421                // In cases where we are extracting links from a resource that has not yet been persisted, the target resource
422                // pid
423                // will be null so we use the target resource id to differentiate instead
424                if (getTargetResourcePid() == null) {
425                        b.append(getTargetResourceId());
426                } else {
427                        b.append(getTargetResourcePid());
428                }
429                return b.toHashCode();
430        }
431
432        @Override
433        public String toString() {
434                StringBuilder b = new StringBuilder();
435                b.append("ResourceLink[");
436                b.append("path=").append(mySourcePath);
437                b.append(", srcResId=").append(mySourceResourcePid);
438                b.append(", srcPartition=").append(myPartitionIdValue);
439                b.append(", srcResTypeId=").append(getSourceResourceTypeId());
440                b.append(", targetResId=").append(myTargetResourcePid);
441                b.append(", targetPartition=").append(myTargetResourcePartitionId);
442                b.append(", targetResType=").append(myTargetResourceType);
443                b.append(", targetResTypeId=").append(getTargetResourceTypeId());
444                b.append(", targetResVersion=").append(myTargetResourceVersion);
445                b.append(", targetResUrl=").append(myTargetResourceUrl);
446
447                b.append("]");
448                return b.toString();
449        }
450
451        public ResourceTable getTargetResource() {
452                return myTransientTargetResource;
453        }
454
455        /**
456         * Creates a clone of this resourcelink which doesn't contain the internal PID
457         * of the target resource.
458         */
459        public ResourceLink cloneWithoutTargetPid() {
460                ResourceLink retVal = new ResourceLink();
461                retVal.mySourceResource = mySourceResource;
462                retVal.mySourceResourcePid = mySourceResource.getId().getId();
463                retVal.mySourceResourceType = mySourceResource.getResourceType();
464                retVal.mySourceResourceTypeId = mySourceResource.getResourceTypeId();
465                retVal.myPartitionIdValue = mySourceResource.getPartitionId().getPartitionId();
466                retVal.mySourcePath = mySourcePath;
467                retVal.myUpdated = myUpdated;
468                retVal.myTargetResourceType = myTargetResourceType;
469                retVal.myTargetResourceTypeId = myTargetResourceTypeId;
470                if (myTargetResourceId != null) {
471                        retVal.myTargetResourceId = myTargetResourceId;
472                } else if (getTargetResource() != null) {
473                        retVal.myTargetResourceId = getTargetResource().getIdDt().getIdPart();
474                        retVal.myTargetResourceTypeId = getTargetResource().getResourceTypeId();
475                }
476                retVal.myTargetResourceUrl = myTargetResourceUrl;
477                retVal.myTargetResourceVersion = myTargetResourceVersion;
478                return retVal;
479        }
480
481        public static ResourceLink forAbsoluteReference(
482                        String theSourcePath, ResourceTable theSourceResource, IIdType theTargetResourceUrl, Date theUpdated) {
483                ResourceLink retVal = new ResourceLink();
484                retVal.setSourcePath(theSourcePath);
485                retVal.setSourceResource(theSourceResource);
486                retVal.setTargetResourceUrl(theTargetResourceUrl);
487                retVal.setUpdated(theUpdated);
488                return retVal;
489        }
490
491        /**
492         * Factory for canonical URL
493         */
494        public static ResourceLink forLogicalReference(
495                        String theSourcePath, ResourceTable theSourceResource, String theTargetResourceUrl, Date theUpdated) {
496                ResourceLink retVal = new ResourceLink();
497                retVal.setSourcePath(theSourcePath);
498                retVal.setSourceResource(theSourceResource);
499                retVal.setTargetResourceUrlCanonical(theTargetResourceUrl);
500                retVal.setUpdated(theUpdated);
501                return retVal;
502        }
503
504        public static ResourceLink forLocalReference(
505                        ResourceLinkForLocalReferenceParams theResourceLinkForLocalReferenceParams) {
506
507                ResourceLink retVal = new ResourceLink();
508                retVal.setSourcePath(theResourceLinkForLocalReferenceParams.getSourcePath());
509                retVal.setSourceResource(theResourceLinkForLocalReferenceParams.getSourceResource());
510                retVal.setTargetResource(
511                                theResourceLinkForLocalReferenceParams.getTargetResourceType(),
512                                theResourceLinkForLocalReferenceParams.getTargetResourcePid(),
513                                theResourceLinkForLocalReferenceParams.getTargetResourceId());
514
515                retVal.setTargetResourcePartitionId(
516                                theResourceLinkForLocalReferenceParams.getTargetResourcePartitionablePartitionId());
517                retVal.setTargetResourceVersion(theResourceLinkForLocalReferenceParams.getTargetResourceVersion());
518                retVal.setUpdated(theResourceLinkForLocalReferenceParams.getUpdated());
519
520                return retVal;
521        }
522
523        public static class ResourceLinkForLocalReferenceParams {
524                private String mySourcePath;
525                private ResourceTable mySourceResource;
526                private String myTargetResourceType;
527                private Long myTargetResourcePid;
528                private String myTargetResourceId;
529                private Date myUpdated;
530                private Long myTargetResourceVersion;
531                private PartitionablePartitionId myTargetResourcePartitionablePartitionId;
532
533                public static ResourceLinkForLocalReferenceParams instance() {
534                        return new ResourceLinkForLocalReferenceParams();
535                }
536
537                public String getSourcePath() {
538                        return mySourcePath;
539                }
540
541                public ResourceLinkForLocalReferenceParams setSourcePath(String theSourcePath) {
542                        mySourcePath = theSourcePath;
543                        return this;
544                }
545
546                public ResourceTable getSourceResource() {
547                        return mySourceResource;
548                }
549
550                public ResourceLinkForLocalReferenceParams setSourceResource(ResourceTable theSourceResource) {
551                        mySourceResource = theSourceResource;
552                        return this;
553                }
554
555                public String getTargetResourceType() {
556                        return myTargetResourceType;
557                }
558
559                public ResourceLinkForLocalReferenceParams setTargetResourceType(String theTargetResourceType) {
560                        myTargetResourceType = theTargetResourceType;
561                        return this;
562                }
563
564                public Long getTargetResourcePid() {
565                        return myTargetResourcePid;
566                }
567
568                public ResourceLinkForLocalReferenceParams setTargetResourcePid(Long theTargetResourcePid) {
569                        myTargetResourcePid = theTargetResourcePid;
570                        return this;
571                }
572
573                public String getTargetResourceId() {
574                        return myTargetResourceId;
575                }
576
577                public ResourceLinkForLocalReferenceParams setTargetResourceId(String theTargetResourceId) {
578                        myTargetResourceId = theTargetResourceId;
579                        return this;
580                }
581
582                public Date getUpdated() {
583                        return myUpdated;
584                }
585
586                public ResourceLinkForLocalReferenceParams setUpdated(Date theUpdated) {
587                        myUpdated = theUpdated;
588                        return this;
589                }
590
591                public Long getTargetResourceVersion() {
592                        return myTargetResourceVersion;
593                }
594
595                /**
596                 * @param theTargetResourceVersion This should only be populated if the reference actually had a version
597                 */
598                public ResourceLinkForLocalReferenceParams setTargetResourceVersion(Long theTargetResourceVersion) {
599                        myTargetResourceVersion = theTargetResourceVersion;
600                        return this;
601                }
602
603                public PartitionablePartitionId getTargetResourcePartitionablePartitionId() {
604                        return myTargetResourcePartitionablePartitionId;
605                }
606
607                public ResourceLinkForLocalReferenceParams setTargetResourcePartitionablePartitionId(
608                                PartitionablePartitionId theTargetResourcePartitionablePartitionId) {
609                        myTargetResourcePartitionablePartitionId = theTargetResourcePartitionablePartitionId;
610                        return this;
611                }
612        }
613}