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.annotation.Nullable;
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.Index;
031import jakarta.persistence.JoinColumn;
032import jakarta.persistence.ManyToOne;
033import jakarta.persistence.SequenceGenerator;
034import jakarta.persistence.Table;
035import jakarta.persistence.Temporal;
036import jakarta.persistence.TemporalType;
037import jakarta.persistence.Transient;
038import org.apache.commons.lang3.Validate;
039import org.apache.commons.lang3.builder.EqualsBuilder;
040import org.apache.commons.lang3.builder.HashCodeBuilder;
041import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
042import org.hl7.fhir.instance.model.api.IIdType;
043
044import java.util.Date;
045
046@Entity
047@Table(
048                name = "HFJ_RES_LINK",
049                indexes = {
050                        // We need to join both ways, so index from src->tgt and from tgt->src.
051                        // From src->tgt, rows are usually written all together as part of ingestion - keep the index small, and
052                        // read blocks as needed.
053                        @Index(name = "IDX_RL_SRC", columnList = "SRC_RESOURCE_ID"),
054                        // But from tgt->src, include all the match columns. Targets will usually be randomly distributed - each row
055                        // in separate block.
056                        @Index(
057                                        name = "IDX_RL_TGT_v2",
058                                        columnList = "TARGET_RESOURCE_ID, SRC_PATH, SRC_RESOURCE_ID, TARGET_RESOURCE_TYPE,PARTITION_ID")
059                })
060public class ResourceLink extends BaseResourceIndex {
061
062        public static final int SRC_PATH_LENGTH = 500;
063        private static final long serialVersionUID = 1L;
064
065        @SequenceGenerator(name = "SEQ_RESLINK_ID", sequenceName = "SEQ_RESLINK_ID")
066        @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_RESLINK_ID")
067        @Id
068        @Column(name = "PID")
069        private Long myId;
070
071        @Column(name = "SRC_PATH", length = SRC_PATH_LENGTH, nullable = false)
072        private String mySourcePath;
073
074        @ManyToOne(optional = false, fetch = FetchType.LAZY)
075        @JoinColumn(
076                        name = "SRC_RESOURCE_ID",
077                        referencedColumnName = "RES_ID",
078                        nullable = false,
079                        foreignKey = @ForeignKey(name = "FK_RESLINK_SOURCE"))
080        private ResourceTable mySourceResource;
081
082        @Column(name = "SRC_RESOURCE_ID", insertable = false, updatable = false, nullable = false)
083        private Long mySourceResourcePid;
084
085        @Column(name = "SOURCE_RESOURCE_TYPE", updatable = false, nullable = false, length = ResourceTable.RESTYPE_LEN)
086        @FullTextField
087        private String mySourceResourceType;
088
089        @ManyToOne(optional = true, fetch = FetchType.LAZY)
090        @JoinColumn(
091                        name = "TARGET_RESOURCE_ID",
092                        referencedColumnName = "RES_ID",
093                        nullable = true,
094                        insertable = false,
095                        updatable = false,
096                        foreignKey = @ForeignKey(name = "FK_RESLINK_TARGET"))
097        private ResourceTable myTargetResource;
098
099        @Column(name = "TARGET_RESOURCE_ID", insertable = true, updatable = true, nullable = true)
100        @FullTextField
101        private Long myTargetResourcePid;
102
103        @Column(name = "TARGET_RESOURCE_TYPE", nullable = false, length = ResourceTable.RESTYPE_LEN)
104        @FullTextField
105        private String myTargetResourceType;
106
107        @Column(name = "TARGET_RESOURCE_URL", length = 200, nullable = true)
108        @FullTextField
109        private String myTargetResourceUrl;
110
111        @Column(name = "TARGET_RESOURCE_VERSION", nullable = true)
112        private Long myTargetResourceVersion;
113
114        @FullTextField
115        @Column(name = "SP_UPDATED", nullable = true) // TODO: make this false after HAPI 2.3
116        @Temporal(TemporalType.TIMESTAMP)
117        private Date myUpdated;
118
119        @Transient
120        private transient String myTargetResourceId;
121
122        /**
123         * Constructor
124         */
125        public ResourceLink() {
126                super();
127        }
128
129        public Long getTargetResourceVersion() {
130                return myTargetResourceVersion;
131        }
132
133        public void setTargetResourceVersion(Long theTargetResourceVersion) {
134                myTargetResourceVersion = theTargetResourceVersion;
135        }
136
137        public String getTargetResourceId() {
138                if (myTargetResourceId == null && myTargetResource != null) {
139                        myTargetResourceId = getTargetResource().getIdDt().getIdPart();
140                }
141                return myTargetResourceId;
142        }
143
144        public String getSourceResourceType() {
145                return mySourceResourceType;
146        }
147
148        public String getTargetResourceType() {
149                return myTargetResourceType;
150        }
151
152        @Override
153        public boolean equals(Object theObj) {
154                if (this == theObj) {
155                        return true;
156                }
157                if (theObj == null) {
158                        return false;
159                }
160                if (!(theObj instanceof ResourceLink)) {
161                        return false;
162                }
163                ResourceLink obj = (ResourceLink) theObj;
164                EqualsBuilder b = new EqualsBuilder();
165                b.append(mySourcePath, obj.mySourcePath);
166                b.append(mySourceResource, obj.mySourceResource);
167                b.append(myTargetResourceUrl, obj.myTargetResourceUrl);
168                b.append(myTargetResourceType, obj.myTargetResourceType);
169                b.append(myTargetResourceVersion, obj.myTargetResourceVersion);
170                // In cases where we are extracting links from a resource that has not yet been persisted, the target resource
171                // pid
172                // will be null so we use the target resource id to differentiate instead
173                if (getTargetResourcePid() == null) {
174                        b.append(getTargetResourceId(), obj.getTargetResourceId());
175                } else {
176                        b.append(getTargetResourcePid(), obj.getTargetResourcePid());
177                }
178                return b.isEquals();
179        }
180
181        @Override
182        public <T extends BaseResourceIndex> void copyMutableValuesFrom(T theSource) {
183                ResourceLink source = (ResourceLink) theSource;
184                mySourcePath = source.getSourcePath();
185                myTargetResource = source.getTargetResource();
186                myTargetResourceId = source.getTargetResourceId();
187                myTargetResourcePid = source.getTargetResourcePid();
188                myTargetResourceType = source.getTargetResourceType();
189                myTargetResourceVersion = source.getTargetResourceVersion();
190                myTargetResourceUrl = source.getTargetResourceUrl();
191        }
192
193        public String getSourcePath() {
194                return mySourcePath;
195        }
196
197        public void setSourcePath(String theSourcePath) {
198                mySourcePath = theSourcePath;
199        }
200
201        public Long getSourceResourcePid() {
202                return mySourceResourcePid;
203        }
204
205        public ResourceTable getSourceResource() {
206                return mySourceResource;
207        }
208
209        public void setSourceResource(ResourceTable theSourceResource) {
210                mySourceResource = theSourceResource;
211                mySourceResourcePid = theSourceResource.getId();
212                mySourceResourceType = theSourceResource.getResourceType();
213        }
214
215        public void setTargetResource(String theResourceType, Long theResourcePid, String theTargetResourceId) {
216                Validate.notBlank(theResourceType);
217
218                myTargetResourceType = theResourceType;
219                myTargetResourcePid = theResourcePid;
220                myTargetResourceId = theTargetResourceId;
221        }
222
223        public String getTargetResourceUrl() {
224                return myTargetResourceUrl;
225        }
226
227        public void setTargetResourceUrl(IIdType theTargetResourceUrl) {
228                Validate.isTrue(theTargetResourceUrl.hasBaseUrl());
229                Validate.isTrue(theTargetResourceUrl.hasResourceType());
230
231                //              if (theTargetResourceUrl.hasIdPart()) {
232                // do nothing
233                //              } else {
234                // Must have set an url like http://example.org/something
235                // We treat 'something' as the resource type because of fix for #659. Prior to #659 fix, 'something' was
236                // treated as the id and 'example.org' was treated as the resource type
237                // Maybe log a warning?
238                //              }
239
240                myTargetResourceType = theTargetResourceUrl.getResourceType();
241                myTargetResourceUrl = theTargetResourceUrl.getValue();
242        }
243
244        public Long getTargetResourcePid() {
245                return myTargetResourcePid;
246        }
247
248        public void setTargetResourceUrlCanonical(String theTargetResourceUrl) {
249                Validate.notBlank(theTargetResourceUrl);
250
251                myTargetResourceType = "(unknown)";
252                myTargetResourceUrl = theTargetResourceUrl;
253        }
254
255        public Date getUpdated() {
256                return myUpdated;
257        }
258
259        public void setUpdated(Date theUpdated) {
260                myUpdated = theUpdated;
261        }
262
263        @Override
264        public Long getId() {
265                return myId;
266        }
267
268        @Override
269        public void setId(Long theId) {
270                myId = theId;
271        }
272
273        @Override
274        public void clearHashes() {
275                // nothing right now
276        }
277
278        @Override
279        public void calculateHashes() {
280                // nothing right now
281        }
282
283        @Override
284        public int hashCode() {
285                HashCodeBuilder b = new HashCodeBuilder();
286                b.append(mySourcePath);
287                b.append(mySourceResource);
288                b.append(myTargetResourceUrl);
289                b.append(myTargetResourceVersion);
290
291                // In cases where we are extracting links from a resource that has not yet been persisted, the target resource
292                // pid
293                // will be null so we use the target resource id to differentiate instead
294                if (getTargetResourcePid() == null) {
295                        b.append(getTargetResourceId());
296                } else {
297                        b.append(getTargetResourcePid());
298                }
299                return b.toHashCode();
300        }
301
302        @Override
303        public String toString() {
304                StringBuilder b = new StringBuilder();
305                b.append("ResourceLink[");
306                b.append("path=").append(mySourcePath);
307                b.append(", src=").append(mySourceResourcePid);
308                b.append(", target=").append(myTargetResourcePid);
309                b.append(", targetType=").append(myTargetResourceType);
310                b.append(", targetVersion=").append(myTargetResourceVersion);
311                b.append(", targetUrl=").append(myTargetResourceUrl);
312
313                b.append("]");
314                return b.toString();
315        }
316
317        public ResourceTable getTargetResource() {
318                return myTargetResource;
319        }
320
321        /**
322         * Creates a clone of this resourcelink which doesn't contain the internal PID
323         * of the target resource.
324         */
325        public ResourceLink cloneWithoutTargetPid() {
326                ResourceLink retVal = new ResourceLink();
327                retVal.mySourceResource = mySourceResource;
328                retVal.mySourceResourcePid = mySourceResource.getId();
329                retVal.mySourceResourceType = mySourceResource.getResourceType();
330                retVal.mySourcePath = mySourcePath;
331                retVal.myUpdated = myUpdated;
332                retVal.myTargetResourceType = myTargetResourceType;
333                if (myTargetResourceId != null) {
334                        retVal.myTargetResourceId = myTargetResourceId;
335                } else if (myTargetResource != null) {
336                        retVal.myTargetResourceId = myTargetResource.getIdDt().getIdPart();
337                }
338                retVal.myTargetResourceUrl = myTargetResourceUrl;
339                retVal.myTargetResourceVersion = myTargetResourceVersion;
340                return retVal;
341        }
342
343        public static ResourceLink forAbsoluteReference(
344                        String theSourcePath, ResourceTable theSourceResource, IIdType theTargetResourceUrl, Date theUpdated) {
345                ResourceLink retVal = new ResourceLink();
346                retVal.setSourcePath(theSourcePath);
347                retVal.setSourceResource(theSourceResource);
348                retVal.setTargetResourceUrl(theTargetResourceUrl);
349                retVal.setUpdated(theUpdated);
350                return retVal;
351        }
352
353        /**
354         * Factory for canonical URL
355         */
356        public static ResourceLink forLogicalReference(
357                        String theSourcePath, ResourceTable theSourceResource, String theTargetResourceUrl, Date theUpdated) {
358                ResourceLink retVal = new ResourceLink();
359                retVal.setSourcePath(theSourcePath);
360                retVal.setSourceResource(theSourceResource);
361                retVal.setTargetResourceUrlCanonical(theTargetResourceUrl);
362                retVal.setUpdated(theUpdated);
363                return retVal;
364        }
365
366        /**
367         * @param theTargetResourceVersion This should only be populated if the reference actually had a version
368         */
369        public static ResourceLink forLocalReference(
370                        String theSourcePath,
371                        ResourceTable theSourceResource,
372                        String theTargetResourceType,
373                        Long theTargetResourcePid,
374                        String theTargetResourceId,
375                        Date theUpdated,
376                        @Nullable Long theTargetResourceVersion) {
377                ResourceLink retVal = new ResourceLink();
378                retVal.setSourcePath(theSourcePath);
379                retVal.setSourceResource(theSourceResource);
380                retVal.setTargetResource(theTargetResourceType, theTargetResourcePid, theTargetResourceId);
381                retVal.setTargetResourceVersion(theTargetResourceVersion);
382                retVal.setUpdated(theUpdated);
383                return retVal;
384        }
385}