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