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