Page MenuHomePhabricator

Implement PropertyRelationRule base class
Closed, ResolvedPublic

Description

Base class for PropertyRelationRules.
Other informations are directly encoded in the documentation of the draft proposal.

Proposal for the interface draft:

/**Base class to standardize/abstract/encapsulate rules and business logic to detect and define
(property/data based) releations in MITK.
Following important definitions must be regarded when using/implementing/specifing rule classes:
- Releations represented by rules are directed relations that point from a source IPropertyOwner (Source)
to a destination IPropertyOwner (Destination).
- A concrete rule ID always "implements" a concrete releation type. E.g. In DICOM the
way to express the source image relation to an input image and to a mask would be nearly the same
and only differs by the encoded purpuse. One may implement an interim or joined class that manages the mutual
stuff, but the registered instances must be one for "DICOM source input image" and one for
"DICOM source mask" and have distinct rule IDs.
- Source may have several relations of a rule to different Destinations.
Destination may have several relations of a rule from different Sources. But a specific Source Destination
pair may have only one relation of a specific rule.
- The deletion of a Destination in the storage does not remove the relation implicitly. It becomes a "zombie" relation
but it should still be documented, even if the destination is unknown. One has to explicitly
disconnect a zombie relation to get rid of it.
- Each releation has its own UID (relationID) that can be used to address it.

The basic concept of the rule design is that we have two layers of relation identification: Layer 1 is the ID-layer
which uses the IIdentifiable interface and UIDs if available to encode "hard" relations. Layer 2 is the Data-layer
which uses the properties of Source and Destination to deduce if there is a relation of the rule type.
The ID-layer is completely implemented by this base class. The base class only falls back to the Data-layer
(implemented by the concrete rule class) if the ID-layer is not sufficient or it is explicitly stated.
Reasons for the introduction of the ID-layer are: 1st, data-defined releations may be weak (several Destinations are
possible; e.g. DICOM source images may point to several loaded mitk images). But if explicitly a releation was connected
it should be deduceable. 2nd, checks on a UID are faster then unnecessary data deduction. 

Rules use relationID properties in order to manage their relationIDs that are stored in the Source.
The relationID-properties follow the following naming schema:
"MITK.Relations.<RuleID>.<InstanceID>.[relationID|destinationID|<data-layer-specific>]"
- <RuleID>: The identifier of the concrete rule that sets the property
- <InstanceID>: The unique index of the relation of the rule for the Source. Used to assign/group the properties to their
relation.
- relationID: The UID of the relation. Set by the ID-layer (so by this class)
- destinationID: The UID of the Destination. Set by the ID-layer (so by this class) if Destination implements IIdentifiable.
- <data-layer-specific>: Information needed by the Data-layer (so derived classes) to find the relationID


*/
class PropertyReleationRuleBase : public itk::Object
{
public:
  mitkClassMacroItkParent(PropertyReleationRuleBase, itk::Object);
  itkCloneMacro(Self);
  
  using RuleIDType = std::string;
  using RelationIDType = IIdentifiable::UIDType;
  using RelationIDVectorType = std::vector<RelationIDType>;
  
  /** Enum class for different types of relations. */
  enum class RelationType {
    None = 0, /**< Two IPropertyOwner have no relation under the rule.*/
    Implicit_Data = 1, /**< Two IPropertyOwner have a relation, but it is only deduced from the Data-layer and they were never explicitly connected.*/
    Connected_Data = 2, /**< Two IPropertyOwner have a relation and there where connected on the Data-layer (so a bit "weaker" as ID). Reasons for the missing ID connection could be that Destintination has not IIdentifiable implemented.*/
    Connected_ID = 4 /**< Two IPropertyOwner have a relation and are fully explictly connected.*/
    }
  
    /** Returns an ID string that identifies the rule class */
  virtual RuleIDType GetRuleID() const = 0;
  
  /** Returns a humanreadable string that can be used to described the rule. Must not be unique.*/
  virtual std::string GetDesplayName() const = 0;  
  
  /** This method checks if owner is eligible to be a Source for the rule. The default implementation returns a
   True for every valid IPropertyOwner (so only a null_ptr results into false). May be reimplement by derived rules if
   they have requirements on potential Sources).*/
  virtual bool IsSourceCandidate(const IPropertyOwner *owner) const;
  
  /** This method checks if owner is eligible to be a Destination for the rule. The default implementation returns a
   True for every valid IPropertyOwner (so only a null_ptr results into false). May be reimplement by derived rules if
   they have requirements on potential Sources).*/
  virtual bool IsDestinationCandidate(const IPropertyOwner *owner) const;
  
  /** Returns true if the passed owner is a Source of a relation defined by the rule so has at least one relation of type
   Connected_Data or Connected_ID.
   @pre owner must be a pointer to a valid IPropertyOwner instance.*/
  bool IsSource(const IPropertyOwner *owner) const;
  
  /** Returns the relation type of the passed IPropertyOwner instances.
   @pre source must be a pointer to a valid IPropertyOwner instance.
   @pre destination must be a pointer to a valid IPropertyOwner instance.
  */
  RelationType HasRelation(const IPropertyOwner *source, const IPropertyOwner *destination) const;
  
  /** Returns a vector of IDs for all relations of this rule that are defined for the passed
   source (so relations of type Connected_Data and Connected_ID).
   *@pre source must be a pointer to a valid IPropertyOwner instance.
  */	
  RelationIDVectorType GetExistingRelations(const IPropertyOwner *source) const;
  
  /** Returns the relation ID for the passed source and destination. If the passed instances have no
   explicit relation (so of type Connected_Data or Connected_ID) no ID can be deduced an exception
   will be thrown.
   @pre source must be a pointer to a valid IPropertyOwner instance.
   @pre destination must be a pointer to a valid IPropertyOwner instance.
   @pre Source and destination have a relation of type Connected_Data or Connected_ID*/
  RelationIDType GetRelationID(const IPropertyOwner *source, const IPropertyOwner *destination) const;
  
  /**Predicate that can be used to find nodes that qualify as source for that rule (but must not be a source yet).
   Thus all nodes where IsSourceCandidate() returns true. */
  NodePredicateBase::ConstPointer GetSourceCandidateIndicator() const;
  /**Predicate that can be used to find nodes that qualify as destination for that rule (but must not be a destination yet).
   Thus all nodes where IsDestinationCandidate() returns true. */
  NodePredicateBase::ConstPointer GetDestinationCandidateIndicator() const;
  /**Predicate that can be used to find nodes that are Sources of that rule and explicitly connected.
   Thus all nodes that have relations of type Connected_Data or Connected_ID.*/
  NodePredicateBase::ConstPointer GetConnectedSourcesDetector() const;
  /**Predicate that can be used to find nodes that are as source related to the passed Destination under the rule
   @param destination Pointer to the Destination instance that should be used for detection.
   @param minimalRelation Defines the minimal strength of the relation type that should be detected.
   @pre Destination must be a valid instance.*/
  NodePredicateBase::ConstPointer GetSourcesDetector(const IPropertyOwner *destination, RelationType minimalRelation = RelationType::Implicit_Data) const;
  /**Predicate that can be used to find nodes that are as Destination related to the passed Source under the rule
   @param source Pointer to the Source instance that should be used for detection.
   @param minimalRelation Defines the minimal strength of the relation type that should be detected.
   @pre Destination must be a valid instance.*/
  NodePredicateBase::ConstPointer GetDestinationsDetector(const IPropertyOwner *source, RelationType minimalRelation = RelationType::Implicit_Data) const;
  /**Returns a predicate that can be used to find the Destination of the passed Source for a given relationID.
   @param source Pointer to the Source instance that should be used for detection.
   @param minimalRelation Defines the minimal strength of the relation type that should be detected.
   @pre source must be a valid instance.
   @pre relationID must identify a relation of the passed source and rule. (This must be in the return of this->GetExistingRelations(source). */
  NodePredicateBase::ConstPointer GetDestinationDetector(const IPropertyOwner *source, RelationIDType relationID) const;
  
  /**Explicitly connects the passed instances. Afterwards they have a relation of Connected_Data or Connected_ID (if a destination implements IIdentifiable).
   If the passed instance are already connected the old connection will be overwritten (and raised to the highest possible connection level).
   @pre source must be a valid instance.
   @pre destination must be a valid instance.*/
  void Connect(IPropertyOwner *source, const IPropertyOwner *destination) const;
  /**Disconnect the passed instances. Afterwards they have a relation of None or Implicit_Data.
   All relationID-properties in the source for the passed destination will be removed.
   @pre source must be a valid instance.
   @pre destination must be a valid instance.*/
  void Disconnect(IPropertyOwner *source, const IPropertyOwner *destination) const;
  /**Disconnect the source from the passed relationID (usefull for "zombie relations").
   All relationID-properties in the source for the passed relationID will be removed.
   If the relationID is not part of the source. Nothing will be changed.
   @pre source must be a valid instance.*/
  void Disconnect(IPropertyOwner *source, RelationIDType relationID) const;

protected:
  
  /** Is called by HasRelation() if no relation is evident in the ID-layer.
   Implement this method to deduce if the passed instances have a relation of type
   None, Implicit_Data or Connected_Data.
   @pre source must be a pointer to a valid IPropertyOwner instance.
   @pre destination must be a pointer to a valid IPropertyOwner instance.
  */
  virtual RelationType HasRelation_datalayer(const IPropertyOwner *source, const IPropertyOwner *destination) const =0;
  
  /** Is called by GetRelationID() if 1) the relation ID cannot be deduced on the ID-layer
   and 2) there are relationID-properties that indicated at least a connected relation.
   Implement this method to check which existing relations as Connected_Data exists between
   both passed instances. If the passed instances have no
   explicit relation (so of type Connected_Data) no ID can be deduced an exception
   will be thrown.
   @pre source must be a pointer to a valid IPropertyOwner instance.
   @pre destination must be a pointer to a valid IPropertyOwner instance.
   @pre Source and destination have a relation of type Connected_Data or Connected_ID*/
  virtual RelationIDType GetRelationID_datalayer(const IPropertyOwner *source, const IPropertyOwner *destination) const;
   
  /**Is called by Connect() to ensure that source has correctly set properties to resample
   the relation. This means that the method should set the properties that describe and encode the relation (relation-properties),
   as well as the relationID-properties that are needed to identify which properties are relation-properties of this relations.
   If the passed instance are already connected the old connection will be overwritten (and raised to the highest possible connection level)
   Connect() will ensure that source and destination are valid pointers.
   @param relationID is the ID for the relation that should be connected. It is queried by Connect() via GetRelationID() or created if needed.
   @pre source must be a valid instance.
   @pre destination must be a valid instance.*/
  virtual void Connect_datalayer(IPropertyOwner *source, const IPropertyOwner *destination, RelationIDType relationID) const = 0;

  /**This method is called by Disconnect() to remove all properties of the relation from the source.
   @remark All relationID-properties of this relation will removed by Disconnect() after this method call.
   If the relationID is not part of the source. Nothing will be changed. Disconnect() ensures that source is a valid pointer if called.
   @pre source must be a valid instance.*/
  virtual void Disconnect_datalayer(IPropertyOwner *source, RelationIDType relationID) const = 0;

};

Remarks to implementation details:

  1. I would propose to use the same UID technique for RelationIDs that is used in IIdentifiable.
  2. Would be sensible to use the PropertyKeyPath class for handling the relationID-properties.

Revisions and Commits

Event Timeline

Please comment on the proposal befor it goes into implementation.

Reworked the interface. One main reason was that after rethinking it made only sense to use PropertyOwner and not any PropertKey directly. Reason: We will have relations rules that are only defined/identified based on a set of properties (e.g. different rules based on DICOM source image references).

Other reasons I tried to motivate direclty in the class documentation.

Read the introduction so far and I have a few considerations in mind:

  • I have to implement a concrete class for each and every relation type? - If this is the case, we might consider providing a GeneralRelationRule class, that encodes the type in a string. For example, I could imagine, that many relations could be expressed this way without the need of implementing many small subclasses just for the purpose of a different name.
  • We should consider providing API for convenient zombie handling like IsOrphaned() or DisconnectOrphanedRelations().
  • Maybe itk::Object is a more suitable base class in case we want to provide events.

Thanks for the comments!

  • I have to implement a concrete class for each and every relation type? - If this is the case, we might consider providing a GeneralRelationRule class, that encodes the type in a string. For example, I could imagine, that many relations could be expressed this way without the need of implementing many small subclasses just for the purpose of a different name.

You are right. Thought in the same direction. I have altered the documentation. Basically it is enough to have distinct rule IDs on the instance level. One must not distinct on the class level. One possibility if you just want to handle different relation types based on the UID is covered by T23805. Is that what you were thinking about?

  • We should consider providing API for convenient zombie handling like IsOrphaned() or DisconnectOrphanedRelations().

For me that would one reason more to introduce a RelationStorageInterface and implement it for the DataStorage. This would be the ideal place to put such queries. What do you think?

  • Maybe itk::Object is a more suitable base class in case we want to provide events.

Changed in the draft.

floca added a revision: Restricted Differential Revision.Jan 30 2018, 4:22 PM