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}