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}