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.entity.TermConceptParentChildLink.RelationshipTypeEnum; 023import ca.uhn.fhir.jpa.model.entity.EntityIndexStatusEnum; 024import ca.uhn.fhir.jpa.model.entity.PartitionablePartitionId; 025import ca.uhn.fhir.jpa.search.DeferConceptIndexingRoutingBinder; 026import ca.uhn.fhir.util.ValidateUtil; 027import ca.uhn.hapi.fhir.sql.hibernatesvc.PartitionedIdProperty; 028import com.google.common.annotations.VisibleForTesting; 029import jakarta.annotation.Nonnull; 030import jakarta.persistence.Column; 031import jakarta.persistence.Embeddable; 032import jakarta.persistence.EmbeddedId; 033import jakarta.persistence.Entity; 034import jakarta.persistence.EnumType; 035import jakarta.persistence.Enumerated; 036import jakarta.persistence.FetchType; 037import jakarta.persistence.ForeignKey; 038import jakarta.persistence.GeneratedValue; 039import jakarta.persistence.GenerationType; 040import jakarta.persistence.Index; 041import jakarta.persistence.JoinColumn; 042import jakarta.persistence.JoinColumns; 043import jakarta.persistence.Lob; 044import jakarta.persistence.ManyToOne; 045import jakarta.persistence.OneToMany; 046import jakarta.persistence.PrePersist; 047import jakarta.persistence.PreUpdate; 048import jakarta.persistence.SequenceGenerator; 049import jakarta.persistence.Table; 050import jakarta.persistence.Temporal; 051import jakarta.persistence.TemporalType; 052import jakarta.persistence.Transient; 053import jakarta.persistence.UniqueConstraint; 054import org.apache.commons.lang3.Validate; 055import org.apache.commons.lang3.builder.EqualsBuilder; 056import org.apache.commons.lang3.builder.HashCodeBuilder; 057import org.apache.commons.lang3.builder.ToStringBuilder; 058import org.apache.commons.lang3.builder.ToStringStyle; 059import org.hibernate.Length; 060import org.hibernate.annotations.JdbcTypeCode; 061import org.hibernate.search.engine.backend.types.Projectable; 062import org.hibernate.search.engine.backend.types.Searchable; 063import org.hibernate.search.mapper.pojo.bridge.IdentifierBridge; 064import org.hibernate.search.mapper.pojo.bridge.ValueBridge; 065import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.IdentifierBridgeRef; 066import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.PropertyBinderRef; 067import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.RoutingBinderRef; 068import org.hibernate.search.mapper.pojo.bridge.mapping.annotation.ValueBridgeRef; 069import org.hibernate.search.mapper.pojo.bridge.runtime.IdentifierBridgeFromDocumentIdentifierContext; 070import org.hibernate.search.mapper.pojo.bridge.runtime.IdentifierBridgeToDocumentIdentifierContext; 071import org.hibernate.search.mapper.pojo.bridge.runtime.ValueBridgeToIndexedValueContext; 072import org.hibernate.search.mapper.pojo.mapping.definition.annotation.DocumentId; 073import org.hibernate.search.mapper.pojo.mapping.definition.annotation.FullTextField; 074import org.hibernate.search.mapper.pojo.mapping.definition.annotation.GenericField; 075import org.hibernate.search.mapper.pojo.mapping.definition.annotation.Indexed; 076import org.hibernate.search.mapper.pojo.mapping.definition.annotation.PropertyBinding; 077import org.hibernate.type.SqlTypes; 078import org.hl7.fhir.r4.model.Coding; 079 080import java.io.Serializable; 081import java.util.ArrayList; 082import java.util.Collection; 083import java.util.Date; 084import java.util.HashSet; 085import java.util.List; 086import java.util.Objects; 087import java.util.Set; 088import java.util.stream.Collectors; 089 090import static java.util.Objects.isNull; 091import static java.util.Objects.nonNull; 092import static org.apache.commons.lang3.StringUtils.left; 093import static org.apache.commons.lang3.StringUtils.length; 094 095@Entity 096@Indexed(routingBinder = @RoutingBinderRef(type = DeferConceptIndexingRoutingBinder.class)) 097@Table( 098 name = "TRM_CONCEPT", 099 uniqueConstraints = { 100 @UniqueConstraint( 101 name = "IDX_CONCEPT_CS_CODE", 102 columnNames = {"PARTITION_ID", "CODESYSTEM_PID", "CODEVAL"}) 103 }, 104 indexes = { 105 @Index(name = "IDX_CONCEPT_INDEXSTATUS", columnList = "INDEX_STATUS"), 106 @Index(name = "IDX_CONCEPT_UPDATED", columnList = "CONCEPT_UPDATED") 107 }) 108public class TermConcept implements Serializable { 109 public static final int MAX_CODE_LENGTH = 500; 110 public static final int MAX_DESC_LENGTH = 400; 111 public static final int MAX_DISP_LENGTH = 500; 112 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TermConcept.class); 113 private static final long serialVersionUID = 1L; 114 115 @OneToMany( 116 fetch = FetchType.LAZY, 117 mappedBy = "myParent", 118 cascade = {}) 119 private List<TermConceptParentChildLink> myChildren; 120 121 @Column(name = "CODEVAL", nullable = false, length = MAX_CODE_LENGTH) 122 @FullTextField( 123 name = "myCode", 124 searchable = Searchable.YES, 125 projectable = Projectable.YES, 126 analyzer = "exactAnalyzer") 127 private String myCode; 128 129 @Temporal(TemporalType.TIMESTAMP) 130 @Column(name = "CONCEPT_UPDATED", nullable = true) 131 private Date myUpdated; 132 133 @ManyToOne(fetch = FetchType.LAZY) 134 @JoinColumns( 135 value = { 136 @JoinColumn( 137 name = "CODESYSTEM_PID", 138 insertable = false, 139 updatable = false, 140 nullable = false, 141 referencedColumnName = "PID"), 142 @JoinColumn( 143 name = "PARTITION_ID", 144 referencedColumnName = "PARTITION_ID", 145 insertable = false, 146 updatable = false, 147 nullable = false) 148 }, 149 foreignKey = @ForeignKey(name = "FK_CONCEPT_PID_CS_PID")) 150 private TermCodeSystemVersion myCodeSystem; 151 152 @Column(name = "CODESYSTEM_PID", insertable = true, updatable = false, nullable = false) 153 @GenericField(name = "myCodeSystemVersionPid") 154 private Long myCodeSystemVersionPid; 155 156 @Column(name = "DISPLAY", nullable = true, length = MAX_DESC_LENGTH) 157 @FullTextField( 158 name = "myDisplay", 159 searchable = Searchable.YES, 160 projectable = Projectable.YES, 161 analyzer = "standardAnalyzer") 162 @FullTextField( 163 name = "myDisplayEdgeNGram", 164 searchable = Searchable.YES, 165 projectable = Projectable.NO, 166 analyzer = "autocompleteEdgeAnalyzer") 167 @FullTextField( 168 name = "myDisplayWordEdgeNGram", 169 searchable = Searchable.YES, 170 projectable = Projectable.NO, 171 analyzer = "autocompleteWordEdgeAnalyzer") 172 @FullTextField( 173 name = "myDisplayNGram", 174 searchable = Searchable.YES, 175 projectable = Projectable.NO, 176 analyzer = "autocompleteNGramAnalyzer") 177 @FullTextField( 178 name = "myDisplayPhonetic", 179 searchable = Searchable.YES, 180 projectable = Projectable.NO, 181 analyzer = "autocompletePhoneticAnalyzer") 182 private String myDisplay; 183 184 @OneToMany(mappedBy = "myConcept", orphanRemoval = false, fetch = FetchType.LAZY) 185 @PropertyBinding(binder = @PropertyBinderRef(type = TermConceptPropertyBinder.class)) 186 private Collection<TermConceptProperty> myProperties; 187 188 @OneToMany(mappedBy = "myConcept", orphanRemoval = false, fetch = FetchType.LAZY) 189 private Collection<TermConceptDesignation> myDesignations; 190 191 @EmbeddedId 192 @DocumentId(identifierBridge = @IdentifierBridgeRef(type = TermConceptPkIdentifierBridge.class)) 193 @GenericField( 194 name = "myId", 195 projectable = Projectable.YES, 196 valueBridge = @ValueBridgeRef(type = TermConceptPkValueBridge.class)) 197 private TermConceptPk myId; 198 199 @Column(name = PartitionablePartitionId.PARTITION_ID, nullable = true, insertable = false, updatable = false) 200 private Integer myPartitionIdValue; 201 202 /** 203 * See {@link EntityIndexStatusEnum} for values 204 */ 205 @Column(name = "INDEX_STATUS", nullable = true) 206 @Enumerated(EnumType.ORDINAL) 207 @JdbcTypeCode(SqlTypes.TINYINT) 208 private EntityIndexStatusEnum myIndexStatus; 209 210 @Deprecated(since = "7.2.0") 211 @Lob 212 @Column(name = "PARENT_PIDS", nullable = true) 213 private String myParentPids; 214 215 @FullTextField( 216 name = "myParentPids", 217 searchable = Searchable.YES, 218 projectable = Projectable.YES, 219 analyzer = "conceptParentPidsAnalyzer") 220 @Column(name = "PARENT_PIDS_VC", nullable = true, length = Length.LONG32) 221 private String myParentPidsVc; 222 223 @OneToMany( 224 cascade = {}, 225 fetch = FetchType.LAZY, 226 mappedBy = "myChild") 227 private List<TermConceptParentChildLink> myParents; 228 229 @Column(name = "CODE_SEQUENCE", nullable = true) 230 private Integer mySequence; 231 232 @Transient 233 private boolean mySupportLegacyLob = false; 234 235 public TermConcept() { 236 super(); 237 } 238 239 public TermConcept(TermCodeSystemVersion theCs, String theCode) { 240 setCodeSystemVersion(theCs); 241 setCode(theCode); 242 } 243 244 public TermConcept addChild(RelationshipTypeEnum theRelationshipType) { 245 TermConcept child = new TermConcept(); 246 child.setCodeSystemVersion(myCodeSystem); 247 addChild(child, theRelationshipType); 248 return child; 249 } 250 251 public TermConceptParentChildLink addChild(TermConcept theChild, RelationshipTypeEnum theRelationshipType) { 252 Validate.notNull(theRelationshipType, "theRelationshipType must not be null"); 253 TermConceptParentChildLink link = new TermConceptParentChildLink(); 254 link.setParent(this); 255 link.setChild(theChild); 256 link.setRelationshipType(theRelationshipType); 257 getChildren().add(link); 258 259 theChild.getParents().add(link); 260 return link; 261 } 262 263 public void addChildren(List<TermConcept> theChildren, RelationshipTypeEnum theRelationshipType) { 264 for (TermConcept next : theChildren) { 265 addChild(next, theRelationshipType); 266 } 267 } 268 269 public TermConceptDesignation addDesignation() { 270 TermConceptDesignation designation = new TermConceptDesignation(); 271 designation.setConcept(this); 272 designation.setCodeSystemVersion(myCodeSystem); 273 getDesignations().add(designation); 274 return designation; 275 } 276 277 private TermConceptProperty addProperty( 278 @Nonnull TermConceptPropertyTypeEnum thePropertyType, 279 @Nonnull String thePropertyName, 280 @Nonnull String thePropertyValue) { 281 Validate.notBlank(thePropertyName); 282 283 TermConceptProperty property = new TermConceptProperty(); 284 property.setConcept(this); 285 property.setCodeSystemVersion(myCodeSystem); 286 property.setType(thePropertyType); 287 property.setKey(thePropertyName); 288 property.setValue(thePropertyValue); 289 if (!getProperties().contains(property)) { 290 getProperties().add(property); 291 } 292 293 return property; 294 } 295 296 public TermConceptProperty addPropertyCoding( 297 @Nonnull String thePropertyName, 298 @Nonnull String thePropertyCodeSystem, 299 @Nonnull String thePropertyCode, 300 String theDisplayName) { 301 return addProperty(TermConceptPropertyTypeEnum.CODING, thePropertyName, thePropertyCode) 302 .setCodeSystem(thePropertyCodeSystem) 303 .setDisplay(theDisplayName); 304 } 305 306 public TermConceptProperty addPropertyString(@Nonnull String thePropertyName, @Nonnull String thePropertyValue) { 307 return addProperty(TermConceptPropertyTypeEnum.STRING, thePropertyName, thePropertyValue); 308 } 309 310 @Override 311 public boolean equals(Object theObj) { 312 if (!(theObj instanceof TermConcept)) { 313 return false; 314 } 315 if (theObj == this) { 316 return true; 317 } 318 319 TermConcept obj = (TermConcept) theObj; 320 321 EqualsBuilder b = new EqualsBuilder(); 322 b.append(myCodeSystem, obj.myCodeSystem); 323 b.append(myCode, obj.myCode); 324 return b.isEquals(); 325 } 326 327 public List<TermConceptParentChildLink> getChildren() { 328 if (myChildren == null) { 329 myChildren = new ArrayList<>(); 330 } 331 return myChildren; 332 } 333 334 public String getCode() { 335 return myCode; 336 } 337 338 public TermConcept setCode(@Nonnull String theCode) { 339 ValidateUtil.isNotBlankOrThrowIllegalArgument(theCode, "theCode must not be null or empty"); 340 ValidateUtil.isNotTooLongOrThrowIllegalArgument( 341 theCode, MAX_CODE_LENGTH, "Code exceeds maximum length (" + MAX_CODE_LENGTH + "): " + length(theCode)); 342 myCode = theCode; 343 return this; 344 } 345 346 public TermCodeSystemVersion getCodeSystemVersion() { 347 return myCodeSystem; 348 } 349 350 public TermConcept setCodeSystemVersion(TermCodeSystemVersion theCodeSystemVersion) { 351 myCodeSystem = theCodeSystemVersion; 352 if (theCodeSystemVersion != null && theCodeSystemVersion.getPid() != null) { 353 myCodeSystemVersionPid = theCodeSystemVersion.getPid(); 354 assert myCodeSystemVersionPid != null; 355 myPartitionIdValue = theCodeSystemVersion.getPartitionId().getPartitionId(); 356 getPid().myPartitionIdValue = myPartitionIdValue; 357 } 358 return this; 359 } 360 361 public List<Coding> getCodingProperties(String thePropertyName) { 362 List<Coding> retVal = new ArrayList<>(); 363 for (TermConceptProperty next : getProperties()) { 364 if (thePropertyName.equals(next.getKey())) { 365 if (next.getType() == TermConceptPropertyTypeEnum.CODING) { 366 Coding coding = new Coding(); 367 coding.setSystem(next.getCodeSystem()); 368 coding.setCode(next.getValue()); 369 coding.setDisplay(next.getDisplay()); 370 retVal.add(coding); 371 } 372 } 373 } 374 return retVal; 375 } 376 377 public Collection<TermConceptDesignation> getDesignations() { 378 if (myDesignations == null) { 379 myDesignations = new ArrayList<>(); 380 } 381 return myDesignations; 382 } 383 384 public String getDisplay() { 385 return myDisplay; 386 } 387 388 public TermConcept setDisplay(String theDisplay) { 389 myDisplay = left(theDisplay, MAX_DESC_LENGTH); 390 return this; 391 } 392 393 public TermConceptPk getPid() { 394 if (myId == null) { 395 myId = new TermConceptPk(); 396 } 397 return myId; 398 } 399 400 public Long getId() { 401 return getPid().myId; 402 } 403 404 public TermConcept setId(Long theId) { 405 getPid().myId = theId; 406 return this; 407 } 408 409 public EntityIndexStatusEnum getIndexStatus() { 410 return myIndexStatus; 411 } 412 413 public TermConcept setIndexStatus(EntityIndexStatusEnum theIndexStatus) { 414 myIndexStatus = theIndexStatus; 415 return this; 416 } 417 418 public String getParentPidsAsString() { 419 return nonNull(myParentPidsVc) ? myParentPidsVc : myParentPids; 420 } 421 422 public List<TermConceptParentChildLink> getParents() { 423 if (myParents == null) { 424 myParents = new ArrayList<>(); 425 } 426 return myParents; 427 } 428 429 public Collection<TermConceptProperty> getProperties() { 430 if (myProperties == null) { 431 myProperties = new ArrayList<>(); 432 } 433 return myProperties; 434 } 435 436 public Integer getSequence() { 437 return mySequence; 438 } 439 440 public TermConcept setSequence(Integer theSequence) { 441 mySequence = theSequence; 442 return this; 443 } 444 445 public List<String> getStringProperties(String thePropertyName) { 446 List<String> retVal = new ArrayList<>(); 447 for (TermConceptProperty next : getProperties()) { 448 if (thePropertyName.equals(next.getKey())) { 449 if (next.getType() == TermConceptPropertyTypeEnum.STRING) { 450 retVal.add(next.getValue()); 451 } 452 } 453 } 454 return retVal; 455 } 456 457 public String getStringProperty(String thePropertyName) { 458 List<String> properties = getStringProperties(thePropertyName); 459 if (properties.size() > 0) { 460 return properties.get(0); 461 } 462 return null; 463 } 464 465 public Date getUpdated() { 466 return myUpdated; 467 } 468 469 public TermConcept setUpdated(Date theUpdated) { 470 myUpdated = theUpdated; 471 return this; 472 } 473 474 @Override 475 public int hashCode() { 476 HashCodeBuilder b = new HashCodeBuilder(); 477 b.append(myCodeSystem); 478 b.append(myCode); 479 return b.toHashCode(); 480 } 481 482 private void parentPids(TermConcept theNextConcept, Set<Long> theParentPids) { 483 for (TermConceptParentChildLink nextParentLink : theNextConcept.getParents()) { 484 TermConcept parent = nextParentLink.getParent(); 485 if (parent != null) { 486 Long parentConceptId = parent.getId(); 487 Validate.notNull(parentConceptId); 488 if (theParentPids.add(parentConceptId)) { 489 parentPids(parent, theParentPids); 490 } 491 } 492 } 493 } 494 495 @SuppressWarnings("unused") 496 @PreUpdate 497 @PrePersist 498 public void prePersist() { 499 if (isNull(myParentPids) && isNull(myParentPidsVc)) { 500 Set<Long> parentPids = new HashSet<>(); 501 TermConcept entity = this; 502 parentPids(entity, parentPids); 503 entity.setParentPids(parentPids); 504 505 ourLog.trace("Code {}/{} has parents {}", entity.getId(), entity.getCode(), entity.getParentPidsAsString()); 506 } 507 508 if (!mySupportLegacyLob) { 509 clearParentPidsLob(); 510 } 511 } 512 513 private void setParentPids(Set<Long> theParentPids) { 514 StringBuilder b = new StringBuilder(); 515 for (Long next : theParentPids) { 516 if (b.length() > 0) { 517 b.append(' '); 518 } 519 b.append(next); 520 } 521 522 if (b.length() == 0) { 523 b.append("NONE"); 524 } 525 526 setParentPids(b.toString()); 527 } 528 529 public TermConcept setParentPids(String theParentPids) { 530 myParentPidsVc = theParentPids; 531 myParentPids = theParentPids; 532 return this; 533 } 534 535 @Override 536 public String toString() { 537 ToStringBuilder b = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE); 538 b.append("pid", myId); 539 b.append("csvPid", myCodeSystemVersionPid); 540 b.append("code", myCode); 541 b.append("display", myDisplay); 542 if (mySequence != null) { 543 b.append("sequence", mySequence); 544 } 545 return b.build(); 546 } 547 548 /** 549 * Returns a view of {@link #getChildren()} but containing the actual child codes 550 */ 551 public List<TermConcept> getChildCodes() { 552 return getChildren().stream().map(TermConceptParentChildLink::getChild).collect(Collectors.toList()); 553 } 554 555 public void flagForLegacyLobSupport(boolean theSupportLegacyLob) { 556 mySupportLegacyLob = theSupportLegacyLob; 557 } 558 559 private void clearParentPidsLob() { 560 myParentPids = null; 561 } 562 563 @VisibleForTesting 564 public boolean hasParentPidsLobForTesting() { 565 return nonNull(myParentPids); 566 } 567 568 public PartitionablePartitionId getPartitionId() { 569 return PartitionablePartitionId.with(myPartitionIdValue, null); 570 } 571 572 public static class TermConceptPkValueBridge implements ValueBridge<TermConceptPk, Long> { 573 @Override 574 public Long toIndexedValue(TermConceptPk value, ValueBridgeToIndexedValueContext context) { 575 return value.myId; 576 } 577 } 578 579 public static class TermConceptPkIdentifierBridge implements IdentifierBridge<TermConceptPk> { 580 @Override 581 public String toDocumentIdentifier( 582 TermConceptPk propertyValue, IdentifierBridgeToDocumentIdentifierContext context) { 583 return Long.toString(propertyValue.myId); 584 } 585 586 @Override 587 public TermConceptPk fromDocumentIdentifier( 588 String documentIdentifier, IdentifierBridgeFromDocumentIdentifierContext context) { 589 TermConceptPk retVal = new TermConceptPk(); 590 retVal.myId = Long.parseLong(documentIdentifier); 591 return retVal; 592 } 593 } 594 595 @Embeddable 596 public static class TermConceptPk implements Serializable { 597 @SequenceGenerator(name = "SEQ_CONCEPT_PID", sequenceName = "SEQ_CONCEPT_PID") 598 @GeneratedValue(strategy = GenerationType.AUTO, generator = "SEQ_CONCEPT_PID") 599 @Column(name = "PID") 600 @GenericField(projectable = Projectable.YES) 601 private Long myId; 602 603 @PartitionedIdProperty 604 @Column(name = PartitionablePartitionId.PARTITION_ID, nullable = false) 605 private Integer myPartitionIdValue; 606 607 /** 608 * Constructor 609 */ 610 public TermConceptPk() { 611 super(); 612 } 613 614 /** 615 * Constructor 616 */ 617 public TermConceptPk(Long theId, Integer thePartitionId) { 618 myId = theId; 619 myPartitionIdValue = thePartitionId; 620 } 621 622 public Integer getPartitionIdValue() { 623 return myPartitionIdValue; 624 } 625 626 public void setPartitionIdValue(Integer thePartitionIdValue) { 627 myPartitionIdValue = thePartitionIdValue; 628 } 629 630 @Override 631 public boolean equals(Object theO) { 632 if (this == theO) { 633 return true; 634 } 635 if (!(theO instanceof TermConceptPk)) { 636 return false; 637 } 638 TermConceptPk that = (TermConceptPk) theO; 639 return Objects.equals(myId, that.myId) && Objects.equals(myPartitionIdValue, that.myPartitionIdValue); 640 } 641 642 @Override 643 public int hashCode() { 644 return Objects.hash(myId, myPartitionIdValue); 645 } 646 647 @Override 648 public String toString() { 649 return myPartitionIdValue + "/" + myId; 650 } 651 652 public Long getId() { 653 return myId; 654 } 655 } 656}