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