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}