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}