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