/* * Licensed under the Apache License, Version 2.0 (the "License"); * * You may not use this file except in compliance with the License. * * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * * See the License for the specific language governing permissions and * limitations under the License. * * Contributions from 2013-2017 where performed either by US government * employees, or under US Veterans Health Administration contracts. * * US Veterans Health Administration contributions by government employees * are work of the U.S. Government and are not subject to copyright * protection in the United States. Portions contributed by government * employees are USGovWork (17USC ยง105). Not subject to copyright. * * Contribution by contractors to the US Veterans Health Administration * during this period are contractually contributed under the * Apache License, Version 2.0. * * See: https://www.usa.gov/government-works * * Contributions prior to 2013: * * Copyright (C) International Health Terminology Standards Development Organisation. * Licensed under the Apache License, Version 2.0. * */ package sh.isaac.provider.query.search; //~--- JDK imports ------------------------------------------------------------ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; //~--- non-JDK imports -------------------------------------------------------- import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sh.isaac.api.Get; import sh.isaac.api.chronicle.LatestVersion; import sh.isaac.api.chronicle.ObjectChronology; import sh.isaac.api.chronicle.ObjectChronologyType; import sh.isaac.api.component.concept.ConceptChronology; import sh.isaac.api.component.concept.ConceptSnapshot; import sh.isaac.api.component.sememe.SememeChronology; import sh.isaac.api.component.sememe.SememeType; import sh.isaac.api.component.sememe.version.DescriptionSememe; import sh.isaac.api.component.sememe.version.DynamicSememe; import sh.isaac.api.component.sememe.version.StringSememe; import sh.isaac.api.coordinate.StampCoordinate; import sh.isaac.api.identity.IdentifiedObject; import sh.isaac.utility.Frills; //~--- classes ---------------------------------------------------------------- /** * Encapsulates a data store search result. * <p> * Logic has been mostly copied from LEGO {@code SnomedSearchResult}. * * @author ocarlsen * @author <a href="mailto:daniel.armbrust.list@gmail.com">Dan Armbrust</a> */ public class CompositeSearchResult { /** The Constant LOG. */ private static final Logger LOG = LoggerFactory.getLogger(CompositeSearchResult.class); //~--- fields -------------------------------------------------------------- /** The containing concept. */ private Optional<ConceptSnapshot> containingConcept = null; /** The matching components. */ private final Set<ObjectChronology<?>> matchingComponents = new HashSet<>(); /** The matching component nid. */ private int matchingComponentNid; /** The best score. */ private float bestScore; // best score, rather than score, as multiple matches may go into a SearchResult //~--- constructors -------------------------------------------------------- /** * Instantiates a new composite search result. * * @param matchingComponentNid the matching component nid * @param score the score */ public CompositeSearchResult(int matchingComponentNid, float score) { this.bestScore = score; // matchingComponent may be null, if the match is not on our view path... this.containingConcept = Optional.empty(); this.matchingComponentNid = matchingComponentNid; } /** * Instantiates a new composite search result. * * @param matchingComponent the matching component * @param score the score */ public CompositeSearchResult(ObjectChronology<?> matchingComponent, float score) { this.matchingComponents.add(matchingComponent); this.bestScore = score; // matchingComponent may be null, if the match is not on our view path... if (matchingComponent == null) { throw new RuntimeException("Please call the constructor that takes a nid, if matchingComponent is null..."); } else { this.matchingComponentNid = matchingComponent.getNid(); } this.containingConcept = locateContainingConcept(matchingComponent.getNid()); } //~--- methods ------------------------------------------------------------- /** * Removes the null results. * * @param results the results * @return the int */ public static int removeNullResults(Collection<CompositeSearchResult> results) { final Collection<CompositeSearchResult> nullResults = new ArrayList<>(); if (results != null) { for (final CompositeSearchResult result: results) { if (result.getContainingConcept() == null) { nullResults.add(result); } } results.removeAll(nullResults); } return nullResults.size(); } /** * To long string. * * @return the string */ public String toLongString() { final StringBuilder builder = new StringBuilder(); builder.append("CompositeSearchResult [containingConcept="); builder.append(this.containingConcept.isPresent() ? this.containingConcept.get() : "null"); builder.append(", matchingComponentNid_="); builder.append(this.matchingComponentNid); builder.append(", bestScore="); builder.append(this.bestScore); builder.append(", getMatchingComponents()="); final List<String> matchingComponentDescs = new ArrayList<>(); for (final ObjectChronology<?> matchingComponent: getMatchingComponents()) { matchingComponentDescs.add((matchingComponent != null) ? matchingComponent.toUserString() : null); } builder.append(matchingComponentDescs); builder.append("]"); return builder.toString(); } /** * To short string. * * @return the string */ public String toShortString() { final StringBuilder builder = new StringBuilder(); builder.append("CompositeSearchResult [containingConcept="); builder.append(this.containingConcept.isPresent() ? this.containingConcept.get() .getNid() : null); if (this.matchingComponentNid != 0) { builder.append(", matchingComponentNid_="); builder.append(this.matchingComponentNid); } builder.append(", bestScore="); builder.append(this.bestScore); if ((this.matchingComponents != null) && (this.matchingComponents.size() > 0)) { builder.append(", matchingComponents="); final List<Integer> matchingComponentNids = new ArrayList<>(); for (final ObjectChronology<?> matchingComponent: this.matchingComponents) { matchingComponentNids.add((matchingComponent != null) ? matchingComponent.getNid() : null); } builder.append(matchingComponentNids); } builder.append("]"); return builder.toString(); } /** * To string with descriptions. * * @return the string */ public String toStringWithDescriptions() { final StringBuilder builder = new StringBuilder(); builder.append("CompositeSearchResult [containingConcept="); String containingConceptDesc = null; if (this.containingConcept.isPresent()) { try { containingConceptDesc = this.containingConcept.get() .getConceptDescriptionText(); } catch (final Exception e) { containingConceptDesc = "{nid=" + this.containingConcept.get().getNid() + "}"; } } builder.append(containingConceptDesc); builder.append(", matchingComponentNid_="); builder.append(this.matchingComponentNid); String matchingComponentDesc = null; if (this.matchingComponentNid != 0) { try { final Optional<? extends ObjectChronology<?>> cc = Get.identifiedObjectService() .getIdentifiedObjectChronology( this.matchingComponentNid); if (cc.isPresent()) { matchingComponentDesc = cc.get() .toUserString(); } } catch (final Exception e) { LOG.warn("Unexpected:", e); } } if (matchingComponentDesc != null) { builder.append(", matchingComponent="); builder.append(matchingComponentDesc); } builder.append(", bestScore="); builder.append(this.bestScore); builder.append(", numMatchingComponents="); final List<Integer> matchingComponentNids = new ArrayList<>(); for (final ObjectChronology<?> matchingComponent: getMatchingComponents()) { matchingComponentNids.add((matchingComponent != null) ? matchingComponent.getNid() : null); } builder.append(matchingComponentNids); builder.append("]"); return builder.toString(); } /** * Adjust score. * * @param newScore the new score */ protected void adjustScore(float newScore) { this.bestScore = newScore; } /** * Merge. * * @param other the other */ protected void merge(CompositeSearchResult other) { if (this.containingConcept.get() .getNid() != other.containingConcept.get().getNid()) { throw new RuntimeException("Unmergeable!"); } if (other.bestScore > this.bestScore) { this.bestScore = other.bestScore; } this.matchingComponents.addAll(other.getMatchingComponents()); } /** * Locate containing concept. * * @param componentNid the component nid * @return the optional */ private Optional<ConceptSnapshot> locateContainingConcept(int componentNid) { final ObjectChronologyType type = Get.identifierService() .getChronologyTypeForNid(componentNid); if (type == ObjectChronologyType.UNKNOWN_NID) { return Optional.empty(); } else if (type == ObjectChronologyType.CONCEPT) { return Frills.getConceptSnapshot(componentNid, null, null); } else if (type == ObjectChronologyType.SEMEME) { return locateContainingConcept(Get.sememeService() .getSememe(componentNid) .getReferencedComponentNid()); } else { throw new RuntimeException("oops"); } } //~--- get methods --------------------------------------------------------- /** * Gets the best score. * * @return the best score */ public float getBestScore() { return this.bestScore; } /** * This may return an empty, if the concept and/or matching component was not on the path. * * @return the containing concept */ public Optional<ConceptSnapshot> getContainingConcept() { return this.containingConcept; } /** * Gets the matching components. * * @return the matching components */ public Set<ObjectChronology<?>> getMatchingComponents() { return this.matchingComponents; } /** * Convenience method to return a filtered list of matchingComponents such that it only returns * Description type components. * * @return the matching description components */ public Set<SememeChronology<DescriptionSememe>> getMatchingDescriptionComponents() { final Set<SememeChronology<DescriptionSememe>> setToReturn = new HashSet<>(); for (final ObjectChronology<?> comp: this.matchingComponents) { if ((comp instanceof SememeChronology<?>) && ((SememeChronology<?>) comp).getSememeType() == SememeType.DESCRIPTION) { setToReturn.add(((SememeChronology<DescriptionSememe>) comp)); } } return Collections.unmodifiableSet(setToReturn); } /** * A convenience method to get string values from the matching Components. * * @param stampCoord the stamp coord * @return the matching strings */ public List<String> getMatchingStrings(Optional<StampCoordinate> stampCoord) { final ArrayList<String> strings = new ArrayList<>(); if (this.matchingComponents.size() == 0) { if (!this.containingConcept.isPresent()) { strings.add("Match to NID (not on path):" + this.matchingComponentNid); } else { throw new RuntimeException("Unexpected"); } } for (final IdentifiedObject iol: this.matchingComponents) { if (iol instanceof ConceptChronology<?>) { // This means they matched on a UUID or other ID lookup. // Return UUID for now - matches on other ID types will be handled differently // in the near future - so ignore the SCTID case for now. strings.add(iol.getPrimordialUuid() .toString()); } else if ((iol instanceof SememeChronology<?>) && ((SememeChronology<?>) iol).getSememeType() == SememeType.DESCRIPTION) { final Optional<LatestVersion<DescriptionSememe>> ds = ((SememeChronology<DescriptionSememe>) iol).getLatestVersion(DescriptionSememe.class, stampCoord.orElse(Get.configurationService() .getDefaultStampCoordinate())); if (ds.isPresent()) { strings.add(ds.get() .value() .getText()); } else { strings.add("No description available on stamp coordinate!"); } } else if ((iol instanceof SememeChronology<?>) && ((SememeChronology<?>) iol).getSememeType() == SememeType.STRING) { final Optional<LatestVersion<StringSememe>> ds = ((SememeChronology<StringSememe>) iol).getLatestVersion(StringSememe.class, stampCoord.orElse(Get.configurationService() .getDefaultStampCoordinate())); if (ds.isPresent()) { strings.add(ds.get() .value() .getString()); } else { strings.add("No sememe available on stamp coordinate!"); } } else if ((iol instanceof SememeChronology<?>) && ((SememeChronology<?>) iol).getSememeType() == SememeType.DYNAMIC) { final Optional<LatestVersion<DynamicSememe>> ds = ((SememeChronology<DynamicSememe>) iol).getLatestVersion(DynamicSememe.class, stampCoord.orElse(Get.configurationService() .getDefaultStampCoordinate())); if (ds.isPresent()) { strings.add(ds.get() .value() .dataToString()); } else { strings.add("No sememe available on stamp coordinate!"); } } else { strings.add("ERROR: No string extractor available for " + iol.getClass().getName()); } } return strings; } }