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