@Collection

Domain semantics for domain object collection.

API

Collection.java
@interface Collection {
  Class<? extends CollectionDomainEvent<?, ?>> domainEvent() default CollectionDomainEvent.Default.class;     (1)
  Where hidden() default Where.NOT_SPECIFIED;     (2)
  Class<?> typeOf() default Object.class;     (3)
}
1 domainEvent

Indicates that changes to the collection that should be posted to the org.apache.isis.applib.services.eventbus.EventBusService event bus using a custom (subclass of) CollectionDomainEvent .

2 hidden

Indicates when the collection is not visible to the user.

3 typeOf

The type-of the elements held within the collection.

Members

domainEvent

Indicates that changes to the collection that should be posted to the org.apache.isis.applib.services.eventbus.EventBusService event bus using a custom (subclass of) CollectionDomainEvent .

Subscribers of this event can interact with the business rule checking (hide, disable, validate) and its modification (before and after).

For example:

public class Order {
  public static class OrderLineItems extends CollectionDomainEvent { ... }

  @CollectionInteraction(OrderLineItems.class)
  public SortedSet<OrderLine> getLineItems() { ...}
}

This subclass must provide a no-arg constructor; the fields are set reflectively.

hidden

Indicates when the collection is not visible to the user.

typeOf

The type-of the elements held within the collection.

This is only provided as a fallback; usually the framework can infer the element type of the collection from the collection method’s generic type.

Examples

For example:

public class ToDoItem {
    public static class DependenciesChangedEvent
            extends CollectionDomainEvent<ToDoItem, ToDoItem> { } (1)
    @Collection(
        domainEvent=DependenciesChangedEvent.class,
        editing = Editing.ENABLED,
        hidden = Where.NOWHERE,                                   (2)
        notPersisted = false,                                     (3)
        typeOf = ToDoItem.class                                   (4)
    )
    public SortedSet<ToDoItem> getDependencies() { /* ... */ }
    ...
}
1 can use no-arg constructor.
2 default value, so could be omitted
3 default value, so could be omitted
4 default value, so could be omitted

The annotation is one of a handful (others including @CollectionLayout, @Property and @PropertyLayout) that can also be applied to the field, rather than the getter method. This is so that boilerplate-busting tools such as Project Lombok can be used.

Usage Notes

Domain events

Whenever a domain object (or list of domain objects) is to be rendered, the framework fires off multiple domain events for every property, collection and action of the domain object. In the cases of the domain object’s collections, the events that are fired are:

  • hide phase: to check that the collection is visible (has not been hidden)

  • disable phase: to check that the collection is usable (has not been disabled)

  • validate phase: to check that the collection’s arguments are valid (to add or remove an element)

  • pre-execute phase: before the modification of the collection

  • post-execute: after the modification of the collection

Subscribers (which must be domain services) subscribe to events posted through the EventBusService, and can influence each of these phases.

The Web UI (Wicket viewer) does not currently support the modification of collections; they are rendered read-only. However, domain events are still relevant to determine if such collections should be hidden.

The workaround is to create add/remove actions and use UI hints to render them close to the collection.

By default the event raised is CollectionDomainEvent.Default. For example:

import lombok.Getter;
import lombok.Setter;

public class ToDoItem {

    @Collection()
    @Getter @Setter
    private SortedSet<ToDoItem> dependencies = ...

    // ...
}

The domainEvent() element allows a custom subclass to be emitted allowing more precise subscriptions (to those subclasses) to be defined instead.

For example:

public class ToDoItem {

    public static class DependenciesChangedEvent
            extends CollectionDomainEvent<ToDoItem, ToDoItem> { } (1)
    @Collection(
        domainEvent=DependenciesChangedEvent.class
    )
    @Getter @Setter
    private SortedSet<ToDoItem> dependencies = ...

    // ...
}
1 inherit from CollectionDomainEvent<T,E> where T is the type of the domain object being interacted with, and E is the type of the element in the collection (both ToDoItem in this example)

The benefit is that subscribers can be more targetted as to the events that they subscribe to.

The framework provides a no-arg constructor and will initialize the domain event using (non-API) setters rather than through the constructor. This substantially reduces the boilerplate in the subclasses because no explicit constructor is required..

Subscribers

Subscribers (which must be domain services) subscribe to events posted through the EventBusService.

Subscribers can be either coarse-grained (if they subscribe to the top-level event type):

import org.springframework.stereotype.Service;
import org.springframework.context.event.EventListener;

@Service
public class SomeSubscriber {
    @EventListener(CollectionDomainEvent.class)
    public void on(CollectionDomainEvent ev) {
        // ...
    }
}

or can be fine-grained (by subscribing to specific event subtypes):

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

@Service
public class SomeSubscriber {
    @EventListener(ToDoItem.DependenciesChangedEvent.class)
    public void on(ToDoItem.DependenciesChangedEvent ev) {
        // ...
    }
}

The subscriber’s method is called (up to) 5 times:

  • whether to veto visibility (hide)

  • whether to veto usability (disable)

  • whether to veto execution (validate) the element being added to/removed from the collection

  • steps to perform prior to the collection being added to/removed from

  • steps to perform after the collection has been added to/removed from

The subscriber can distinguish these by calling ev.getEventPhase(). Thus the general form is:

 import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;

@Service
public class SomeSubscriber {

    @EventListener(CollectionDomainEvent.class)
    public void on(CollectionDomainEvent ev) {
        switch(ev.getEventPhase()) {

            case HIDE:                      (1)
                break;
            case DISABLE:                   (2)
                break;
            case VALIDATE:                  (3)
                break;

            case EXECUTING:
                break;
            case EXECUTED:
                break;
        }
    }
}
1 call ev.hide() or ev.veto("") to hide the collection
2 call ev.disable("…​") or ev.veto("…​") to disable the collection
3 call ev.invalidate("…​") or ev.veto("…​") if object being added/removed to collection is invalid

It is also possible to abort the transaction during the executing or executed phases by throwing an exception. If the exception is a subtype of RecoverableException then the exception will be rendered as a user-friendly warning (eg Growl/toast) rather than an error.

Default, Doop and Noop events

If the domainEvent() element is not explicitly specified (is left as its default value, `CollectionDomainEvent.Default), then the framework will, by default, post an event.

If this is not required, then the isis.reflector.facet.collectionAnnotation.domainEvent.postForDefault configuration collection can be set to "false"; this will disable posting.

On the other hand, if the domainEvent has been explicitly specified to some subclass, then an event will be posted. The framework provides CollectionDomainEvent.Doop as such a subclass, so setting the domainEvent attribute to this class will ensure that the event to be posted, irrespective of the configuration collection setting.

And, conversely, the framework also provides CollectionDomainEvent.Noop; if domainEvent attribute is set to this class, then no event will be posted.

Raising events programmatically

Normally events are only raised for interactions through the UI. However, events can be raised programmatically either by calling the EventBusService API directly, or by emulating the UI by wrapping the target object using the WrapperFactory domain service.

Hiding Collections

Collections can be hidden at the domain-level, indicating that they are not visible to the end-user. This is accomplished using the hidden element.

The acceptable values are:

  • Where.EVERYWHERE or Where.ANYWHERE

    The collection should be hidden everywhere.

  • Where.ANYWHERE

    Synonym for everywhere.

  • Where.OBJECT_FORMS

    The collection should be hidden when displayed within an object form.

  • Where.NOWHERE

    The collection should not be hidden.

The other values of the Where enum have no meaning for a collection.

For example:

import lombok.Getter;
import lombok.Setter;

public class Customer {

    @Collection(where=Where.EVERYWHERE)
    @Getter @Setter
    private SortedSet<Address> addresses = ...

}

Alternatives

It is also possible to use @CollectionLayout#hidden or using file-based layout such that the collection can be hidden at the view layer.

Collection type

The typeOf element specifies the expected type of an element contained within a collection when for whatever reason the type cannot be inferred from the generic type, or to provide a hint about the actual run-time (as opposed to compile-time) type.

For example:

import lombok.Getter;

public void Customer {

    @Collection(typeOf=Order.class)
    @Getter
    private SortedSet outstandingOrders = ...

}

In general though this element should not be necessary if you use generics for the return type, eg:

import lombok.Getter;

public void Customer {

    @Collection()
    @Getter
    private SortedSet<Order> outstandingOrders = ...

}