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