001/*
002 * #%L
003 * HAPI FHIR JPA Server
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.entity;
021
022import ca.uhn.fhir.jpa.model.entity.BasePartitionable;
023import ca.uhn.fhir.jpa.model.entity.IdAndPartitionId;
024import ca.uhn.fhir.util.ValidateUtil;
025import com.google.common.annotations.VisibleForTesting;
026import jakarta.annotation.Nonnull;
027import jakarta.persistence.Column;
028import jakarta.persistence.Entity;
029import jakarta.persistence.EnumType;
030import jakarta.persistence.Enumerated;
031import jakarta.persistence.FetchType;
032import jakarta.persistence.ForeignKey;
033import jakarta.persistence.GeneratedValue;
034import jakarta.persistence.GenerationType;
035import jakarta.persistence.Id;
036import jakarta.persistence.IdClass;
037import jakarta.persistence.Index;
038import jakarta.persistence.JoinColumn;
039import jakarta.persistence.JoinColumns;
040import jakarta.persistence.Lob;
041import jakarta.persistence.ManyToOne;
042import jakarta.persistence.PrePersist;
043import jakarta.persistence.SequenceGenerator;
044import jakarta.persistence.Table;
045import org.apache.commons.lang3.Validate;
046import org.apache.commons.lang3.builder.EqualsBuilder;
047import org.apache.commons.lang3.builder.HashCodeBuilder;
048import org.apache.commons.lang3.builder.ToStringBuilder;
049import org.apache.commons.lang3.builder.ToStringStyle;
050import org.hibernate.Length;
051import org.hibernate.annotations.JdbcTypeCode;
052import org.hibernate.search.engine.backend.types.Projectable;
053import org.hibernate.search.engine.backend.types.Searchable;
054import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField;
055import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField;
056import org.hibernate.type.SqlTypes;
057import org.hibernate.validator.constraints.NotBlank;
058
059import java.io.Serializable;
060import java.nio.charset.StandardCharsets;
061
062import static java.util.Objects.nonNull;
063import static org.apache.commons.lang3.StringUtils.left;
064import static org.apache.commons.lang3.StringUtils.length;
065
066@Entity
067@Table(
068                name = "TRM_CONCEPT_PROPERTY",
069                uniqueConstraints = {},
070                indexes = {
071                        // must have same name that indexed FK or SchemaMigrationTest complains because H2 sets this index
072                        // automatically
073                        @Index(name = "FK_CONCEPTPROP_CONCEPT", columnList = "CONCEPT_PID", unique = false),
074                        @Index(name = "FK_CONCEPTPROP_CSV", columnList = "CS_VER_PID")
075                })
076@IdClass(IdAndPartitionId.class)
077public class TermConceptProperty extends BasePartitionable implements Serializable {
078        public static final int MAX_PROPTYPE_ENUM_LENGTH = 6;
079        private static final long serialVersionUID = 1L;
080        public static final int MAX_LENGTH = 500;
081
082        @ManyToOne(fetch = FetchType.LAZY)
083        @JoinColumns(
084                        value = {
085                                @JoinColumn(
086                                                name = "CONCEPT_PID",
087                                                referencedColumnName = "PID",
088                                                insertable = false,
089                                                updatable = false,
090                                                nullable = false),
091                                @JoinColumn(
092                                                name = "PARTITION_ID",
093                                                referencedColumnName = "PARTITION_ID",
094                                                insertable = false,
095                                                updatable = false,
096                                                nullable = false)
097                        },
098                        foreignKey = @ForeignKey(name = "FK_CONCEPTPROP_CONCEPT"))
099        private TermConcept myConcept;
100
101        @Column(name = "CONCEPT_PID", insertable = true, updatable = true, nullable = false)
102        private Long myConceptPid;
103
104        /**
105         * TODO: Make this non-null
106         *
107         * @since 3.5.0
108         */
109        @ManyToOne(fetch = FetchType.LAZY)
110        @JoinColumns(
111                        value = {
112                                @JoinColumn(
113                                                name = "CS_VER_PID",
114                                                insertable = false,
115                                                updatable = false,
116                                                nullable = false,
117                                                referencedColumnName = "PID"),
118                                @JoinColumn(
119                                                name = "PARTITION_ID",
120                                                referencedColumnName = "PARTITION_ID",
121                                                insertable = false,
122                                                updatable = false,
123                                                nullable = false)
124                        },
125                        foreignKey = @ForeignKey(name = "FK_CONCEPTPROP_CSV"))
126        private TermCodeSystemVersion myCodeSystemVersion;
127
128        @Column(name = "CS_VER_PID")
129        private Long myCodeSystemVersionPid;
130
131        @Id()
132        @SequenceGenerator(name = "SEQ_CONCEPT_PROP_PID", sequenceName = "SEQ_CONCEPT_PROP_PID")
133        @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_CONCEPT_PROP_PID")
134        @Column(name = "PID")
135        private Long myId;
136
137        @Column(name = "PROP_KEY", nullable = false, length = MAX_LENGTH)
138        @NotBlank
139        @GenericField(searchable = Searchable.YES)
140        private String myKey;
141
142        @Column(name = "PROP_VAL", nullable = true, length = MAX_LENGTH)
143        @FullTextField(searchable = Searchable.YES, projectable = Projectable.YES, analyzer = "standardAnalyzer")
144        @GenericField(name = "myValueString", searchable = Searchable.YES)
145        private String myValue;
146
147        @Deprecated(since = "7.2.0")
148        @Column(name = "PROP_VAL_LOB")
149        @Lob()
150        private byte[] myValueLob;
151
152        @Column(name = "PROP_VAL_BIN", nullable = true, length = Length.LONG32)
153        private byte[] myValueBin;
154
155        @Enumerated(EnumType.ORDINAL)
156        @Column(name = "PROP_TYPE", nullable = false)
157        @JdbcTypeCode(SqlTypes.INTEGER)
158        private TermConceptPropertyTypeEnum myType;
159
160        /**
161         * Relevant only for properties of type {@link TermConceptPropertyTypeEnum#CODING}
162         */
163        @Column(name = "PROP_CODESYSTEM", length = MAX_LENGTH, nullable = true)
164        private String myCodeSystem;
165
166        /**
167         * Relevant only for properties of type {@link TermConceptPropertyTypeEnum#CODING}
168         */
169        @Column(name = "PROP_DISPLAY", length = MAX_LENGTH, nullable = true)
170        @GenericField(name = "myDisplayString", searchable = Searchable.YES)
171        private String myDisplay;
172
173        /**
174         * Constructor
175         */
176        public TermConceptProperty() {
177                super();
178        }
179
180        /**
181         * Relevant only for properties of type {@link TermConceptPropertyTypeEnum#CODING}
182         */
183        public String getCodeSystem() {
184                return myCodeSystem;
185        }
186
187        /**
188         * Relevant only for properties of type {@link TermConceptPropertyTypeEnum#CODING}
189         */
190        public TermConceptProperty setCodeSystem(String theCodeSystem) {
191                ValidateUtil.isNotTooLongOrThrowIllegalArgument(
192                                theCodeSystem,
193                                MAX_LENGTH,
194                                "Property code system exceeds maximum length (" + MAX_LENGTH + "): " + length(theCodeSystem));
195                myCodeSystem = theCodeSystem;
196                return this;
197        }
198
199        /**
200         * Relevant only for properties of type {@link TermConceptPropertyTypeEnum#CODING}
201         */
202        public String getDisplay() {
203                return myDisplay;
204        }
205
206        /**
207         * Relevant only for properties of type {@link TermConceptPropertyTypeEnum#CODING}
208         */
209        public TermConceptProperty setDisplay(String theDisplay) {
210                myDisplay = left(theDisplay, MAX_LENGTH);
211                return this;
212        }
213
214        public String getKey() {
215                return myKey;
216        }
217
218        public TermConceptProperty setKey(@Nonnull String theKey) {
219                ValidateUtil.isNotBlankOrThrowIllegalArgument(theKey, "theKey must not be null or empty");
220                ValidateUtil.isNotTooLongOrThrowIllegalArgument(
221                                theKey, MAX_LENGTH, "Code exceeds maximum length (" + MAX_LENGTH + "): " + length(theKey));
222                myKey = theKey;
223                return this;
224        }
225
226        public TermConceptPropertyTypeEnum getType() {
227                return myType;
228        }
229
230        public TermConceptProperty setType(@Nonnull TermConceptPropertyTypeEnum theType) {
231                Validate.notNull(theType);
232                myType = theType;
233                return this;
234        }
235
236        /**
237         * This will contain the value for a {@link TermConceptPropertyTypeEnum#STRING string}
238         * property, and the code for a {@link TermConceptPropertyTypeEnum#CODING coding} property.
239         */
240        public String getValue() {
241                if (hasValueBin()) {
242                        return getValueBinAsString();
243                }
244                return myValue;
245        }
246
247        /**
248         * This will contain the value for a {@link TermConceptPropertyTypeEnum#STRING string}
249         * property, and the code for a {@link TermConceptPropertyTypeEnum#CODING coding} property.
250         */
251        public TermConceptProperty setValue(String theValue) {
252                if (theValue.length() > MAX_LENGTH) {
253                        setValueBin(theValue);
254                } else {
255                        myValueLob = null;
256                        myValueBin = null;
257                }
258                myValue = left(theValue, MAX_LENGTH);
259                return this;
260        }
261
262        public boolean hasValueBin() {
263                if (myValueBin != null && myValueBin.length > 0) {
264                        return true;
265                }
266
267                if (myValueLob != null && myValueLob.length > 0) {
268                        return true;
269                }
270                return false;
271        }
272
273        public TermConceptProperty setValueBin(byte[] theValueBin) {
274                myValueBin = theValueBin;
275                myValueLob = theValueBin;
276                return this;
277        }
278
279        public TermConceptProperty setValueBin(String theValueBin) {
280                return setValueBin(theValueBin.getBytes(StandardCharsets.UTF_8));
281        }
282
283        public String getValueBinAsString() {
284                if (myValueBin != null && myValueBin.length > 0) {
285                        return new String(myValueBin, StandardCharsets.UTF_8);
286                }
287
288                return new String(myValueLob, StandardCharsets.UTF_8);
289        }
290
291        public TermConceptProperty setCodeSystemVersion(TermCodeSystemVersion theCodeSystemVersion) {
292                myCodeSystemVersion = theCodeSystemVersion;
293                myCodeSystemVersionPid = theCodeSystemVersion.getPid();
294                return this;
295        }
296
297        public TermConceptProperty setConcept(TermConcept theConcept) {
298                myConcept = theConcept;
299                myConceptPid = theConcept.getId();
300                setPartitionId(theConcept.getPartitionId());
301                return this;
302        }
303
304        @PrePersist
305        public void prePersist() {
306                if (myConceptPid == null) {
307                        myConceptPid = myConcept.getId();
308                        assert myConceptPid != null;
309                }
310        }
311
312        @Override
313        public String toString() {
314                return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
315                                .append("conceptPid", myConcept.getId())
316                                .append("key", myKey)
317                                .append("value", getValue())
318                                .toString();
319        }
320
321        @Override
322        public boolean equals(Object theO) {
323                if (this == theO) {
324                        return true;
325                }
326
327                if (theO == null || getClass() != theO.getClass()) {
328                        return false;
329                }
330
331                TermConceptProperty that = (TermConceptProperty) theO;
332
333                return new EqualsBuilder()
334                                .append(myKey, that.myKey)
335                                .append(myValue, that.myValue)
336                                .append(myType, that.myType)
337                                .append(myCodeSystem, that.myCodeSystem)
338                                .append(myDisplay, that.myDisplay)
339                                .isEquals();
340        }
341
342        @Override
343        public int hashCode() {
344                return new HashCodeBuilder(17, 37)
345                                .append(myKey)
346                                .append(myValue)
347                                .append(myType)
348                                .append(myCodeSystem)
349                                .append(myDisplay)
350                                .toHashCode();
351        }
352
353        public Long getPid() {
354                return myId;
355        }
356
357        public IdAndPartitionId getPartitionedId() {
358                return IdAndPartitionId.forId(myId, this);
359        }
360
361        public void performLegacyLobSupport(boolean theSupportLegacyLob) {
362                if (!theSupportLegacyLob) {
363                        myValueLob = null;
364                }
365        }
366
367        @VisibleForTesting
368        public boolean hasValueBlobForTesting() {
369                return nonNull(myValueLob);
370        }
371
372        @VisibleForTesting
373        public void setValueBlobForTesting(byte[] theValueLob) {
374                myValueLob = theValueLob;
375        }
376
377        @VisibleForTesting
378        public boolean hasValueBinForTesting() {
379                return nonNull(myValueBin);
380        }
381
382        @VisibleForTesting
383        public void setValueBinForTesting(byte[] theValuebin) {
384                myValueBin = theValuebin;
385        }
386
387        public Long getId() {
388                return myId;
389        }
390}