Hints and Tips

This chapter provides some solutions for problems we’ve encountered ourselves or have been raised on the Apache Isis mailing lists.

'Are you sure?' idiom

Sometimes an action might perform irreversible changes. In such a case it’s probably a good idea for the UI to require that the end-user explicitly confirms that they intended to invoke the action.

One way to meet this requirement is using the framework’s built-in @Action#semantics attribute:

@Action(
        semantics = SemanticsOf.IDEMPOTENT_ARE_YOU_SURE
)
public SimpleObject updateName(
        @Parameter(maxLength = NAME_LENGTH)
        @ParameterLayout(named = "New name")
        final String name) {
    setName(name);
    return this;
}

This will render as:

action semantics are you sure

Custom CSS

Custom CSS styles can be associated with specific regions of the layout:

<grid ...>
    <row>
        <col span="2" unreferencedActions="true">
            <ns2:domainObject/>
            <row>
                <col span="12"
                     cssClass="custom-width-100">       (1)
                    <ns2:action id="exportToWordDoc"/>
                </col>
            </row>
            ...
        </col>
        <col span="5" unreferencedCollections="true"
             cssClass="custom-padding-top-20">          (2)
            ...
        </col>
        <col span="5"
            cssClass="custom-padding-top-20">           (3)
            ...
        </col>
    </row>
</grid>
1 Render the column with the custom-width-100 CSS class.
2 Render the column with the custom-padding-top-20 CSS class.
3 Ditto

For example the custom-width-100 style is used to "stretch" the button for the exportToWordDoc action in the left-most column. For the Web UI (Wicket viewer) this is accomplished with custom CSS, usually CSS in the static/css/application.css file:

.custom-width-100 ul,
.custom-width-100 ul li,
.custom-width-100 ul li a.btn {
    width: 100%;
}

Similarly, the middle and right columns are rendered using the custom-padding-top-20 CSS class. This shifts them down from the top of the page slightly, using the following CSS:

.custom-padding-top-20 {
    padding-top: 20px;
}

Overriding Default Service Implns

The framework provides default implementations for many of the domain services. This is convenient, but sometimes you will want to replace the default implementation with your own service implementation.

For example, suppose you wanted to provide your own implementation of LocaleProvider. The trick is to use the @javax.annotation.Priority annotation.

Here’s how:

import org.springframework.stereotype.Service;
import org.springframework.core.Ordered;

@Service
@Priority(PriorityPrecedence.EARLY)                                (1)
public class MyLocaleProvider implements LocaleProvider {
    @Override
    public Locale getLocale() {
        // ...
    }
}
1 PriorityPrecedence (in the Apache Isis applib) provides some standard values.

Decorating existing implementations

It’s sometimes useful to decorate the existing implementation (ie have your own implementation delegate to the default); this is quite easy to imlpement:

import org.springframework.stereotype.Service;
import org.springframework.core.Ordered;

@Service
@Priority(PriorityPrecedence.FIRST)                             (1)
public class MyLocaleProvider implements LocaleProvider {

    @Inject List<LocaleProvider> localeProviders;           (2)

    @Override
    public Locale getLocale() {
        return localeProviders.stream()                     (3)
                .filter(x -> x != this)                     (4)
                .findFirst()                                (5)
                .map(LocaleProvider::getLocale)             (6)
                .orElseThrow(RuntimeException::new);        (7)
    }
}
1 takes precedence over the default implementation when injected elsewhere.
2 injects all implementations, including this implementation
3 streams over all implementations…​
4 ...ignoring this one…​
5 ...uses the next one…​
6 ...and delegate to it.
7 Fails fast if no other implementations available (should not happen if framework provides a default implementation).

None of the default implementations provided by the framework use Ordered(OrderPrecedence.HIGHEST), so all can be overridden if required.

Vetoing Visibility

The framework provides a number of actions (domain service menu items or mixins) that you may wish to suppress from the user interface. This can be done by implementing a "vetoing subscriber" design pattern.

For example, the BookmarkService has a related interface, BookmarkHolder, for objects that holds a reference to another domain object implicitly as a Bookmark.

The BookmarkHolder_object mixin surfaces the related domain object as an "object" property of the BookmarkHolder.

If you want to suppress this property, a vetoing subscriber can listen to the associated domain event of the mixin:

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

@Service
public class HideBookmarkHolderObjectProperty {

    @EventListener(BookmarkHolder_object.PropertyDomainEvent.class)
    public void on(BookmarkHolder_object.PropertyDomainEvent ev) {
        ev.hide();
    }
}

These domain events will often have a superclass, in which case the vetoing subscriber can be as broad-brush or fine-grained as required.

Transactions and Errors

In Apache Isis, every interaction (action invocation or property edit) is automatically wrapped in a transaction, and any repository query automatically does a flush before hand.

What that means is that there’s no need to explicitly start or commit transactions in Apache Isis; this will be done for you. Indeed, if you do try to manage transactions (eg by reaching into the JDO PersistenceManager exposed by the JdoSupportService domain service, then you are likely to confuse the framework and get a stack trace for your trouble.

However, you can complete a given transaction and start a new one. This is sometimes useful if writing a fixture script which is going to perform some sort of bulk migration of data from an old system. For this use case, use the TransactionService.

For example:

public class SomeLongRunningFixtureScript extends FixtureScript {

    protected void execute(final ExecutionContext executionContext) {
        // do some work
        transactionService.nextTransaction();
        // do some work
        transactionService.nextTransaction();
        // do yet more work
    }

    @javax.inject.Inject
    TransactionService transactionService;
}

Raise message in the UI

The framework provides the MessageService as a means to return an out-of-band message to the end-user. In the Web UI (Wicket viewer) these are shown as "toast" pop-ups; the REST API (Restful Objects viewer) returns an HTTP header.

The UserService provides three APIs, for different:

  • informUser() - an informational message. In the Wicket viewer these are short-lived pop-ups that disappear after a short time.

  • warnUser() - a warning. In the Wicket viewer these do not auto-close; they must be acknowledged.

  • raiseError() - an error. In the Wicket viewer these do not auto-close; they must be acknowledged.

Each pop-up has a different background colour indicating its severity.

None of these messages/errors has any influence on the transaction; any changes to objects will be committed.

Aborting transactions

If you want to abort Apache Isis' transaction, this can be done by throwing an exception. The exception message is displayed to the user on the error page (if Web UI (Wicket viewer)) or a 500 status error code (if the Restful Objects viewer).

If the exception thrown is because of an unexpected error (eg a NullPointerException in the domain app itself), then the error page will include a stack trace. If however you want to indicate that the exception is in some sense "expected", then throw a RecoverableException (or any subclass, eg ApplicationException); the stack trace will then be suppressed from the error page.

Another way in which exceptions might be considered "expected" could be as the result of attempting to persist an object which then violates some type of database constraint. Even if the domain application checks beforehand, it could be that another user operating on the object at the same moment of time might result in the conflict.

To handle this the ExceptionRecognizer SPI can be used. The framework provides a number of implementations out-of-the-box; whenever an exception is thrown it is passed to each known recognizer implementation to see if it recognizes the exception and can return a user-meaningful error message. For example, ExceptionRecognizerForSQLIntegrityConstraintViolationUniqueOrIndexException checks if the exception inherits from java.sql.SQLIntegrityConstraintViolationException, and if so, constructs a suitable message.

Persisted Title

Normally the title of an object is not persisted to the database, rather it is recomputed automatically from underlying properties. On occasion though you might want the title to also be persisted; either to make things easier for the DBA, or for an integration scenario, or to support full-text search.

We can implement this feature by either overriding the lifecycle methods or (probably better) subscribing to the lifecycle events.

In the design we discuss here we make it a responsibility of the entities to persist the title as a property, by implementing a ObjectWithPersistedTitle interface:

public interface ObjectWithPersistedTitle {
    @Programmatic                                   (1)
    String getPersistedTitle();
    void setPersistedTitle(final String title);
}
1 we don’t want to expose this in the UI.

We can then define a subscribing domain service that leverage this.

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.apache.isis.applib.events.lifecycle.ObjectPersistingEvent;
import org.apache.isis.applib.events.lifecycle.ObjectUpdatingEvent;
import lombok.val;

@Service
public class TitlingService {

    @EventListener(ObjectPersistingEvent.class)
    public void on(ObjectPersistingEvent ev) {
        handle(ev.getSource());
    }

    @EventListener(ObjectUpdatingEvent.class)
    public void on(ObjectUpdatingEvent ev) {
        handle(ev.getSource());
    }

    private void handle(final Object persistentInstance) {
        if(persistentInstance instanceof ObjectWithPersistedTitle) {
            val objectWithPersistedTitle =
                (ObjectWithPersistedTitle) persistentInstance;
            objectWithPersistedTitle.setPersistedTitle(titleService.titleOf(objectWithPersistedTitle));
        }
    }

    @Inject
    TitleService titleService;
}

The above is probably the easiest and most straightforward design. One could imagine other designs where the persisted title is stored elsewhere, such as a Apache Lucene (or similar) database to allow for full-text searches.

How to handle void/null results

From this thread on the Apache Isis users mailing list:

  • When using a void action, let’s say a remove action, the user is redirected to a page "no results". When clicking the back button in the browser the user sees "Object not found" (since you’ve just deleted this object).

  • You can return a list for example to prevent the user from being redirect to a "No results" page, but I think it’s not the responsibility of the controllers in the domain model.

  • A solution could be that wicket viewer goes back one page when encountering a deleted object. And refresh the current page when receiving a null response or invoking a void action. But how to implement this?

One way to implement this idea is to provide a custom implementation of the RoutingService SPI domain service. The default implementation will either return the current object (if not null), else the home page (as defined by @HomePage) if one exists.

The following custom implementation refines this to use the breadcrumbs (available in the Wicket viewer) to return the first non-deleted domain object found in the list of breadcrumbs:

import org.springframework.stereotype.Service;
import lombok.val;

@Service
@Priority(PriorityPrecedence.EARLY)                                              (1)
public class RoutingServiceUsingBreadcrumbs
                  extends RoutingServiceDefault {
    @Override
    public Object route(final Object original) {
        if(original != null) {                                            (2)
            return original;
        }
        repositoryService.flush();                                        (3)

        val breadcrumbModelProvider =                                     (4)
            (BreadcrumbModelProvider) AuthenticatedWebSession.get();
        val breadcrumbModel = breadcrumbModelProvider.getBreadcrumbModel();
        final List<EntityModel> breadcrumbs = breadcrumbModel.getList();

        val firstViewModelOrNonDeletedPojoIfAny =
                breadcrumbs.stream()                                      (5)
                .filter(entityModel -> entityModel != null)
                .map(EntityModel::getObject)                              (6)
                .filter(Objects::nonNull)
                .map(ManagedObject::getObject)                            (7)
                .filter(pojo ->
                  metamodelService.sortOf(pojo, RELAXED) == VIEW_MODEL)   (8)
                .findFirst();

        return firstViewModelOrNonDeletedPojoIfAny.orElse(homePage());    (9)
    }
    private Object homePage() {
        return homePageResolverService.homePage();
    }

    @Inject HomePageResolverService homePageResolverService;
    @Inject MetaModelService metaModelService;
    @Inject RepositoryService repositoryService;
}
1 override the default imlpementation
2 if a non-null object was returned, then return this
3 ensure that any persisted objects have been deleted.
4 reach inside the Wicket viewer’s internals to obtain the list of breadcrumbs.
5 loop over all breadcrumbs
6 unwrap the Wicket viewer’s serializable representation of each domain object (EntityModel) to the Isis runtime’s representation (ManagedObject)
7 unwrap the Isis runtime’s representation of each domain object (ManagedObject) to the domain object pojo itself
8 if object is persistable (not a view model) then make sure it is not deleted
9 return the first object if any, otherwise the home page object (if any).

Subclass properties in tables

Suppose you have a hierarchy of classes where a property is derived and abstract in the superclass, concrete implementations in the subclasses. For example:

public abstract class LeaseTerm {
    public abstract BigDecimal getEffectiveValue();
    ...
}

public class LeaseTermForIndexableTerm extends LeaseTerm {
    public BigDecimal getEffectiveValue() { /* ... */ }
    ...
}

Currently the Wicket viewer will not render the property in tables (though the property is correctly rendered in views).

The work-around is simple enough; make the method concrete in the superclass and return a dummy implementation, eg:

public abstract class LeaseTerm {
    public BigDecimal getEffectiveValue() {
        return null;
    }
    ...
}

Alternatively the implementation could throw a RuntimeException (since the method is not intended to be called).

How to implement a spellchecker?

From this thread on the Apache Isis users mailing list:

  • What is the easiest way to add a spell checker to the text written in a field in a domain object, for instance to check English syntax?

One way to implement is to use the event bus:

if if the change is made through an edit, you can use @Property#domainEvent.

You’ll need some way to know which fields should be spell checked. Two ways spring to mind:

  • either look at the domain event’s identifier

  • or subclass the domain event (recommended anyway) and have those subclass events implement some sort of marker interface, eg a SpellCheckEvent.

And you’ll (obviously) also need some sort of spell checker implementation to call.