Skip to content
Martin Ledvinka edited this page May 27, 2024 · 10 revisions

JOPA entity classes are much like JPA entity classes - they are regular POJO classes with some mapping annotations. The following sections provide more info about the mapping and other features related to entities.

Mapping

JOPA entities use mapping based on OWL, but RDFS ontologies can be just as easily mapped. The following table shows an overview of the mapping.

Ontology JOPA annotation Field type
OWL/RDFS Class @OWLClass -
Object property @OWLObjectProperty Another entity, URI
Datatype property @OWLDataProperty Primitive type wrapper class, String, Date, Java 8 Datetime
Annotation property @OWLAnnotationProperty Another entity, URI, Primitive type wrapper class, String, Date, Java 8 Datetime

Additionally, unmapped ontological data can be accessed using two special fields:

  • @Types - a set of ontological types besides the one mapped by @OWLClass.
  • @Properties - a map of property to set-of-values allowing to access properties not mapped by the above described explicit mapping.

Example

The following snippet shows all the above mentioned mapping features in one class.

@OWLClass(iri = SKOS.CONCEPT)
public class Term {
  @Id
  private URI id;

  @OWLAnnotationProperty(iri = SKOS.PREF_LABEL)
  private MultilingualString label;

  @OWLAnnotationProperty(iri = SKOS.DEFINITION)
  private MultilingualString definition;

  @OWLObjectProperty(iri = SKOS.NARROWER)
  private Set<Term> children;

  @OWLDataProperty(iri = SKOS.NOTATION, simpleLiteral = true)
  private Set<String> notations;

  @Inferred
  @OWLObjectProperty(iri = SKOS.RELATED)
  private Set<Term> related;

  @Types
  private Set<String> types;

  @Properties
  private Map<String, Set<String>> properties;

}

Plural Attributes

Plural attributes are never null when loaded by JOPA. If there are not values for them, empty collections are returned.

Inferred Attributes

Entity attributes annotated with @Inferred can contain values inferred by the underlying repository's reasoner. The annotation has a parameter allowing you to specify whether explicit (asserted) property values should be included when the attribute value is loaded. By default, it is true. In the listing above, the related attribute will contain terms whose relatedness was asserted on the side of this term as well as those whose relatedness was asserted on their side (because skos:related is symmetric).

Since JOPA 0.20.0, inferred attribute values are editable. However, this editing has limits. Adding new values is always ok, but when a value is removed, a check is performed whether the value being removed is inferred or asserted. If it is inferred, an exception (InferredAttributeModifiedException) is thrown.

Prior to JOPA 0.20.0, attributes marked with @Inferred were always read only and any changes to their values (even adding a new value) resulted in an InferredAttributeModifiedException.

Java Enums

Java enums can be mapped in three ways that are specified via the @Enumerated annotation:

  • EnumType.STRING (default) - uses the name of the enum constant as a string literal in the repository. It is also used when no @Enumerated annotation is specified on an enum-valued attribute.
  • EnumType.ORDINAL - uses the ordinal value of the enum constant in the repository (since 0.21.0).
  • EnumType.OBJECT_ONE_OF - enum constants are mapped to OWL individuals (RDF resources). This represents the owl:ObjectOneOf (OWL 2 reference, sec. 8.1.4)(since 0.21.0).

The string and ordinal enum types are used on attributes with @OWLDataProperty and @OWLAnnotationProperty mapping, ObjectOneOf are used on @OWLObjectProperty attributes. Here is a short example of an enum mapped to OWL individuals:

public enum OwlPropertyType {

  @Individual(iri = OWL.DATATYPE_PROPERTY)
  DATATYPE_PROPERTY,
  @Individual(iri = OWL.ANNOTATION_PROPERTY)
  OBJECT_PROPERTY,
  @Individual(iri = OWL.ANNOTATION_PROPERTY)
  ANNOTATION_PROPERTY
}

And its use in an entity:

@Enumerated(EnumType.OBJECT_ONE_OF)
@OWLObjectProperty(iri = "http://example.org/has-property-type")
private OwlPropertyType objectOneOf;

Inheritance

JOPA entity classes support three kinds of inheritance mapping:

  1. Mapped superclass
  2. Class-based
  3. Interface-based

Note that in case of proper ontological inheritance (2. and 3.), JOPA expects the repository to contain data about the class hierarchy for polymorphism to fully work. For example, if the application asks for all instances of a superclass, the repository has to contain class assertions (asserted or inferred) of instances of its subclasses also instances of the superclass for them to be loaded. JOPA will not insert such statements. The repository must either infer them based on the ontological model, or the application must insert them explicitly (e.g., via the @Types field).

Mapped Superclass Inheritance

Mapped superclasses (annotated with @MappedSuperclass) can be used to group attributes that should be shared by other entity classes in the model. However, a mapped superclass has no identity in the underlying ontology, as it is not mapped to any ontological classes. It is more of a technical device.

Class-based Inheritance

Class-based inheritance uses classical Java class inheritance. In case of an object model, it means that a class mapped to an ontological class is extended by another class mapped to an ontological class. The goals is to replicate the relationships between classes from the ontology on the object model level.

Interface-based Inheritance

Interface-based inheritance allows to work around Java's restriction of single class inheritance. Conceptual models often use multiple inheritance to represent the reality of the underlying domain. Since Java allows one class to implement multiple interfaces, JOPA uses this to support this mapping. To use it, declare an interface, annotate it with @OWLClass for class mapping and declare getters/setters in the interface, annotated with property mapping annotations (@OWLDataProperty etc.) - it does not make a difference if getters or setters are annotated. Implementing classes then need to declare corresponding fields. These need not be annotated, since the mapping is taken from the interface.

For example:

@OWLClass(iri = "ex:SocialBeing")
interface Person {
  @OWLDataProperty(iri = "foaf:firstName")
  String getFirstName();
  @OWLDataProperty(iri = "foaf:familyName")
  String getLastName();
}

@OWLClass(iri = "ex:Customer")
interface Customer {
  @OWLDataProperty(iri = "ex:customerIdentifier")
  void setCustomerId(Integer customerId);
}

@OWLClass(iri = "ex:PersonCustomer")
class PersonCustomer implements Person, Customer {
  @Id
  private URI id;
  private String firstName;
  private String lastName;
  private Integer customerId;

  // Implement inherited getters and setters.

See the thesis by Bc. Jan Kolovecký who implemented multiple inheritance in JOPA for details.

Lifecycle Listeners

Much like JPA, JOPA supports entity lifecycle listeners. These come in two flavors:

  • Lifecycle callback methods declared on entity classes or in mapped superclasses
  • Entity listener classes

The following annotations are bound to the respective entity lifecycle events:

  • @PrePersist
  • @PostPersist
  • @PreRemove
  • @PostRemove
  • @PreUpdate
  • @PostUpdate
  • @PostLoad

Lifecycle Callbacks

Lifecycle callbacks are methods declared directly in entity classes or in mapped superclasses, annotated with the aforementioned annotations. A callback may be annotated by multiple lifecycle event annotations.

The callback may not be static, but can have arbitrary visibility. It should take no argument, as it is called on the entity instance to which the lifecycle event applies. The callback's return type must be void. Method signature should thus look as follows:

void callback();

Entity Listeners

Entity listeners are non-managed classes containing lifecycle callback methods. An entity listeners is registered for a concrete entity through the @EntityListeners annotation declared on the entity. Multiple listeners can be specified for an entity, the order of declaration specifying also the order in which their callback methods are invoked.

The callback methods are similar to those defined directly in entities (see above) but they take a single argument representing the entity for which the callback is being invoked. The type of the argument can be Object or a more specific type. The method signature should look as like this:

void callback(Object entity);

The callback method are annotated with lifecycle event annotations listed above.

Lifecycle Callback Methods Semantics

The semantics of lifecycle callback methods correspond to the semantics defined in JPA, section 3.5.3, so details can be found there. In a nutshell, PrePersist and PreRemove are invoked before the respective entity manager operations are executed. PostPersist and PostRemove are invoked after the entity has been made persistent/removed.

PreUpdate and PostUpdate occur before and after new entity state has been merged into the underlying repository. Note that if the lifecycle listener modifies the state of the entity on which it was invoked (e.g. when setting provenance data on update), the listener is not invoked again. This is to prevent endless loops.

PostLoad is invoked after the entity has been loaded into the persistence context. It also applies to query results. In case of lazily loaded attributes, the PostLoad will occur when the relationship is triggered and the entity is loaded.

All the lifecycle operations are cascaded.

Note that EntityManager is not available in lifecycle callbacks and attempts to access it (e.g. via CDI in entities) are discouraged, as they may interfere with the entity processing logic.