Typed Graph

Bryn Cooke edited this page Jun 10, 2013 · 1 revision

Frames can also be configured to store type-information in properties, and to use that information runtime when instantiating objects. For example, consider an object model for relating persons to the pets they own:

interface Person extends VertexFrame {
  @Adjacency(label = "pet") Iterable<? extends Animal> getPets();
  @Adjacency(label = "pet") void addPet(Animal animal);
}

interface Animal extends VertexFrame {
}

interface Fish extends Animal {
}

The Problem

Without any extra information Frames isn’t aware of the actual types of Animal objects. If you call person.getPets(), all returned objects are Animal-instances. It’s not possible to instantiate them as Fish, or Dog, or other sub-classes of Animal, based on runtime information. Likewise, when you call person.addPet(aFish), the fact that this particular pet is actually a Fish will be lost.

Frames can Store and Use Type Data

Since Frames 2.4.0 Frames has support for instantiating polymorphic types, to tackle the above problem, and make runtime decisions about what interfaces to proxy. This is done by telling Frames what the name of each type is, and in which property field that name should be stored:

  1. The root of your class hierarchy (in this case the Animal interface), should be annotated with a TypeField annotation. That annotation contains the property-key in which type information is stored.
  2. Each concrete (instantiatable) sub-type should be annotated with a TypeValue annotation. That annotation holds the property value that is stored in the property with the key from TypeField.
  3. A TypedGraphModuleBuilder can be used to make Frames aware of these annotations.

This may lead to the following interface definitions for our Person → Pet graph:

interface Person extends VertexFrame {
  @Adjacency(label = "pet") Iterable<? extends Animal> getPets();
  @Adjacency(label = "pet") void addPet(Animal animal);
}

@TypeField("type") interface Animal extends VertexFrame {
}

interface Mammal extends Animal {}

@TypeValue("dog") interface Dog extends Mammal {}

@TypeValue("shepherd") interface Shepherd extends Dog {}

@TypeValue("cat") interface Cat extends Mammal {}

@TypeValue("fish") interface Fish extends Animal {}

Please note that only Animal has a TypeField annotation, because it is the root of our hierarchy. You are not allowed to change the property-key used for storing type information in subtypes of Animal. Also note that both Animal and Mammal don’t have TypeValue annotations. This is because pets are typically subtypes of Animal/Mammal, e.g. a Cat, or a Fish. It is not forbidden to have a TypeValue annotation on the same interface that holds the TypeField annotation. It just doesn’t make sense in this particular example. Please also note that it’s perfectly valid to have TypeValue annotations on subtypes of interfaces that already have a TypeValue annotation, like the Shepherd above, which is a specific type of Dog.

With these extra annotations, Frames is now able to automatically add a type=fish property when storing a vertex that represents a Fish, or type=dog for a Dog. Additionally, Frames may return sub-types of Animal, using the actual property value for type. Thus person.getPets() now returns Dogs, Fishes, etc.

Using TypedGraphModuleBuilder

For Frames to actually use the TypeField and TypeValue annotations, a FramedGraph has to know about the interfaces annotated with TypeValue. Since there is no efficient way in Java to get all interfaces that have a specific annotation, your code should tell a FramedGraph where to find those interfaces. For the animal hierarchy in the previous example this may be done as follows:

static final FramedGraphFactory FACTORY = new FramedGraphFactory(
    new TypedGraphModuleBuilder()
        .withClass(Fish.class)
        .withClass(Cat.class)
        .withClass(Dog.class)
        .withClass(Shepherd.class)
        .build()
);

Please note that only the interfaces with TypeValue annotations are given. Frames will detect the TypeField by itself. The FACTORY created this way is reusable and thread-safe once fully initialized. It can be used to create FramedGraphs as follows:

FramedGraph<Graph> framedGraph = FACTORY.create(graph);

FAQ for Polymorphic Types

What happens when I frame an element that doesn’t have a value for the property defined in TypeField?

Frames will just instantiate the compile time supplied interface (i.e. Animal in the previous examples).

What happens when I frame an element that has an unknown value for the property defined in TypeValue (thus a corresponding TypeValue is missing or not registered)?

Frames will ignore the value, hence will instantiate the compile time supplied interface (i.e. Animal in the previous examples).

What happens when I create a Frame for an interface that has a TypeField in the hierarchy, but no (registered) TypeValue annotation?

No type information will be stored. Thus if you later frame that element, it will be framed as an instance of the compile time supplied interface.