Class AnnotationSetDisposition

java.lang.Object
ubic.gemma.model.common.AbstractIdentifiable
ubic.gemma.model.common.auditAndSecurity.curation.AnnotationSetDisposition
All Implemented Interfaces:
Identifiable

@Entity public class AnnotationSetDisposition extends AbstractIdentifiable

One ruling on one finding inside an AnnotationSet.

A curator working an audit rules on each finding in turn — FindingDisposition.ACCEPTED / FindingDisposition.DISMISSED / FindingDisposition.NEEDS_MORE_INFO, with a reason. The finding is named by findingId when the producer supplies one and by targetId otherwise; both are the producer's own strings. Gemma does not parse the payload and so cannot validate either.

🛑 Set, finding and element are three scopes and this is the middle one. AnnotationSetTriage rules on the whole set; CurationDraftDispositions.Disposition classifies one element within one draft and is derived rather than stored. All three read like dispositions and none of them substitutes for another.

Append-only, latest wins — deliberately the opposite choice from AnnotationSetTriage, which keys UNIQUE(set, judge) and upserts. There, the question is what a judge currently thinks about a whole set. Here the sequence is itself the record: a finding that was accepted, then dismissed after a second look, is a different history from one dismissed outright, and an audit's value depends on being able to see that a ruling moved. standing(Collection) applies the latest-wins fold on read.

judgeKind reuses TriageJudgeKind rather than declaring a second enum with the same two values. Its name says triage and its meaning — did a person or a machine decide this — is the same question in both places.

No unique constraint, so nothing here stops two rows with the same (set, targetId, decidedBy). That is the point.

  • Field Details

    • NEWEST_FIRST

      public static final Comparator<AnnotationSetDisposition> NEWEST_FIRST
      Most recent first, id breaking a same-millisecond tie. The same ordering the DAO applies in SQL, kept here so an in-memory fold cannot disagree with a query.
  • Constructor Details

    • AnnotationSetDisposition

      public AnnotationSetDisposition()
  • Method Details

    • standing

      public static List<AnnotationSetDisposition> standing(@Nullable Collection<AnnotationSetDisposition> rulings)
      The standing ruling under each standingKey() — the most recent row per finding, or per target for rows carrying no finding id.

      Ties on decidedAt break on id, so two rulings written in the same millisecond still resolve to one answer rather than to whichever the collection happened to yield first.

      Returns:
      the standing rulings, most recent first. Findings nobody has ruled on are simply absent.
    • getAnnotationSet

      public AnnotationSet getAnnotationSet()
    • setAnnotationSet

      public void setAnnotationSet(AnnotationSet annotationSet)
    • getTargetId

      public String getTargetId()
    • setTargetId

      public void setTargetId(String targetId)
    • getFindingId

      @Nullable public String getFindingId()
    • setFindingId

      public void setFindingId(@Nullable String findingId)
    • standingKey

      public String standingKey()
      The key this ruling supersedes others under: the finding when it has one, the target when it does not.

      🛑 The two are deliberately NOT coalesced into one namespace. A ruling on a finding and a ruling on a target are not rulings on the same thing, so a finding id that happened to equal some target id must not make one supersede the other — that would invent a supersession nobody performed. The discriminating prefix is what guarantees it, rather than an argument that the two id spaces never collide.

    • getDisposition

      public FindingDisposition getDisposition()
    • setDisposition

      public void setDisposition(FindingDisposition disposition)
    • getDecidedBy

      public String getDecidedBy()
    • setDecidedBy

      public void setDecidedBy(String decidedBy)
    • getJudgeKind

      public TriageJudgeKind getJudgeKind()
    • setJudgeKind

      public void setJudgeKind(TriageJudgeKind judgeKind)
    • getDecidedAt

      public Date getDecidedAt()
    • setDecidedAt

      public void setDecidedAt(Date decidedAt)
    • getReason

      @Nullable public String getReason()
    • setReason

      public void setReason(@Nullable String reason)
    • hashCode

      public int hashCode()
      Constant, deliberately — the same choice, for the same reasons, as AnnotationSetTriage.hashCode().

      Hashing the business key would mean hashing annotationSet, a LAZY proxy whose hashCode() can force initialization outside a session. An id-based hash is worse: the id flips from null to a value on persist, so a ruling added to a HashSet while transient lands in the wrong bucket and contains() then answers false for an element that is in the set.

      The cost is one bucket degrading to a linear scan. An audit carries tens of findings, not thousands.

      Specified by:
      hashCode in class AbstractIdentifiable
    • equals

      public boolean equals(Object o)
      By id once both sides have one, otherwise by (set, targetId, decidedBy, decidedAt). The annotation set is compared by id rather than by object so a lazy proxy on either side does not have to be initialized to answer.

      The fallback includes decidedAt because these rows are append-only: without it, a curator's second ruling on the same finding would compare equal to their first and silently collapse in a Set.

      Specified by:
      equals in class AbstractIdentifiable