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