<
Fork me on GitHub

1. Core Concepts

This introductory chapter should give you a good about what Apache Isis actually is: the fundamental ideas and principles that it builds upon, how it compares with other frameworks, what the fundamental building blocks are for actually writing an Isis application, and what services and features the framework provides for you to leverage in your own apps.

Parts of this chapter have been adapted from Dan Haywood’s 2009 book, 'Domain Driven Design using Naked Objects'. We’ve also added some new insights and made sure the material we’ve used is relevant to Apache Isis.

1.1. Philosophy and Architecture

This section describes some of the core ideas and architectural patterns upon which Apache Isis builds.

1.1.1. Domain-Driven Design

This section describes some of the core ideas and architectural patterns upon which Apache Isis builds.

There’s no doubt that we developers love the challenge of understanding and deploying complex technologies. But understanding the nuances and subtleties of the business domain itself is just as great a challenge, perhaps more so. If we devoted our efforts to understanding and addressing those subtleties, we could build better, cleaner, and more maintainable software that did a better job for our stakeholders. And there’s no doubt that our stakeholders would thank us for it.

A couple of years back Eric Evans wrote his book Domain-Driven Design, which is well on its way to becoming a seminal work. In fact, most if not all of the ideas in Evans' book have been expressed before, but what he did was pull those ideas together to show how predominantly object-oriented techniques can be used to develop rich, deep, insightful, and ultimately useful business applications.

There are two central ideas at the heart of domain-driven design.

  • the ubiquitous language is about getting the whole team (both domain experts and developers) to communicate more transparently using a domain model.

  • Meanwhile, model-driven design is about capturing that model in a very straightforward manner in code.

Let’s look at each in turn.

Ubiquitous Language

It’s no secret that the IT industry is plagued by project failures. Too often systems take longer than intended to implement, and when finally implemented, they don’t address the real requirements anyway.

Over the years we in IT have tried various approaches to address this failing. Using waterfall methodologies, we’ve asked for requirements to be fully and precisely written down before starting on anything else. Or, using agile methodologies, we’ve realized that requirements are likely to change anyway and have sought to deliver systems incrementally using feedback loops to refine the implementation.

But let’s not get distracted talking about methodologies. At the end of the day what really matters is communication between the domain experts (that is, the business) who need the system and the techies actually implementing it. If the two don’t have and cannot evolve a shared understanding of what is required, then the chance of delivering a useful system will be next to nothing.

Bridging this gap is traditionally what business analysts are for; they act as interpreters between the domain experts and the developers. However, this still means there are two (or more) languages in use, making it difficult to verify that the system being built is correct. If the analyst mistranslates a requirement, then neither the domain expert nor the application developer will discover this until (at best) the application is first demonstrated or (much worse) an end user sounds the alarm once the application has been deployed into production.

Rather than trying to translate between a business language and a technical language, with DDD we aim to have the business and developers using the same terms for the same concepts in order to create a single domain model. This domain model identifies the relevant concepts of the domain, how they relate, and ultimately where the responsibilities are. This single domain model provides the vocabulary for the ubiquitous language for our system.

Ubiquitous Language

Build a common language between the domain experts and developers by using the concepts of the domain model as the primary means of communication. Use the terms in speech, in diagrams, in writing, and when presenting.

If an idea cannot be expressed using this set of concepts, then go back and extend the model. Look for and remove ambiguities and inconsistencies.

Creating a ubiquitous language calls upon everyone involved in the system’s development to express what they are doing through the vocabulary provided by the model. If this can’t be done, then our model is incomplete. Finding the missing words deepens our understanding of the domain being modeled.

This might sound like nothing more than me insisting that the developers shouldn’t use jargon when talking to the business. Well, that’s true enough, but it’s not a one-way street. A ubiquitous language demands that the developers work hard to understand the problem domain, but it also demands that the business works hard in being precise in its naming and descriptions of those concepts. After all, ultimately the developers will have to express those concepts in a computer programming language.

Also, although here I’m talking about the "domain experts" as being a homogeneous group of people, often they may come from different branches of the business. Even if we weren’t building a computer system, there’s a lot of value in helping the domain experts standardize their own terminology. Is the marketing department’s "prospect" the same as sales' "customer," and is that the same as an after-sales "contract"?

The need for precision within the ubiquitous language also helps us scope the system. Most business processes evolve piecemeal and are often quite ill-defined. If the domain experts have a very good idea of what the business process should be, then that’s a good candidate for automation, that is, including it in the scope of the system. But if the domain experts find it hard to agree, then it’s probably best to leave it out. After all, human beings are rather more capable of dealing with fuzzy situations than computers.

So, if the development team (business and developers together) continually searches to build their ubiquitous language, then the domain model naturally becomes richer as the nuances of the domain are uncovered. At the same time, the knowledge of the business domain experts also deepens as edge conditions and contradictions that have previously been overlooked are explored.

We use the ubiquitous language to build up a domain model. But what do we do with that model? The answer to that is the second of our central ideas.

Model-Driven Design

Of the various methodologies that the IT industry has tried, many advocate the production of separate analysis models and implementation models. One example (from the mid 2000s) was that of the OMG's Model-Driven Architecture ( MDA) initiative, with its platform-independent model (the PIM) and a platform-specific model (the PSM).

Bah and humbug! If we use our ubiquitous language just to build up a high-level analysis model, then we will re-create the communication divide. The domain experts and business analysts will look only to the analysis model, and the developers will look only to the implementation model. Unless the mapping between the two is completely mechanical, inevitably the two will diverge.

What do we mean by model anyway? For some, the term will bring to mind UML class or sequence diagrams and the like. But this isn’t a model; it’s a visual representation of some aspect of a model. No, a domain model is a group of related concepts, identifying them, naming them, and defining how they relate. What is in the model depends on what our objective is. We’re not looking to simply model everything that’s out there in the real world. Instead, we want to take a relevant abstraction or simplification of it and then make it do something useful for us. A model is neither right nor wrong, just more or less useful.

For our ubiquitous language to have value, the domain model that encodes it must have a straightforward, literal representation to the design of the software, specifically to the implementation. Our software’s design should be driven by this model; we should have a model-driven design.

Model-Driven Design

There must be a straightforward and very literal way to represent the domain model in terms of software. The model should balance these two requirements: form the ubiquitous language of the development team and be representable in code.

Changing the code means changing the model; refining the model requires a change to the code.

Here also the word design might mislead; some might again be thinking of design documents and design diagrams. But by design we mean a way of organizing the domain concepts, which in turn leads to the way in which we organize their representation in code.

Luckily, using object-oriented ( OO) languages such as Java, this is relatively easy to do; OO is based on a modeling paradigm anyway. We can express domain concepts using classes and interfaces, and we can express the relationships between those concepts using associations.

So far so good. Or maybe, so far so much motherhood and apple pie. Understanding the DDD concepts isn’t the same as being able to apply them, and some of the DDD ideas can be difficult to put into practice. Time to discuss the naked objects pattern and how it eases that path by applying these central ideas of DDD in a very concrete way.

1.1.2. Naked Objects Pattern

Apache Isis implements the naked objects pattern, originally formulated by Richard Pawson. So who better than Richard to explain the origination of the idea?

The Naked Objects pattern arose, at least in part, from my own frustration at the lack of success of the domain-driven approach. Good examples were hard to find—​as they are still.

A common complaint from DDD practitioners was that it was hard to gain enough commitment from business stakeholders, or even to engage them at all. My own experience suggested that it was nearly impossible to engage business managers with UML diagrams. It was much easier to engage them in rapid prototyping — where they could see and interact with the results — but most forms of rapid prototyping concentrate on the presentation layer, often at the expense of the underlying model and certainly at the expense of abstract thinking.

Even if you could engage the business sponsors sufficiently to design a domain model, by the time you’d finished developing the system on top of the domain model, most of its benefits had disappeared. It’s all very well creating an agile domain object model, but if any change to that model also dictates the modification of one or more layers underneath it (dealing with persistence) and multiple layers on top (dealing with presentation), then that agility is practically worthless.

The other concern that gave rise to the birth of Naked Objects was how to make user interfaces of mainstream business systems more "expressive" — how to make them feel more like using a drawing program or CAD system. Most business systems are not at all expressive; they treat the user merely as a dumb process-follower, rather than as an empowered problem-solver. Even the so-called usability experts had little to say on the subject: try finding the word "empowerment" or any synonym thereof in the index of any book on usability. Research had demonstrated that the best way to achieve expressiveness was to create an object-oriented user interface (OOUI). In practice, though, OOUIs were notoriously hard to develop.

Sometime in the late 1990s, it dawned on me that if the domain model really did represent the "ubiquitous language" of the business and those domain objects were behaviorally rich (that is, business logic is encapsulated as methods on the domain objects rather than in procedural scripts on top of them), then the UI could be nothing more than a reflection of the user interface. This would solve both of my concerns. It would make it easier to do domain-driven design, because one could instantly translate evolving domain modeling ideas into a working prototype. And it would deliver an expressive, object-oriented user interface for free. Thus was born the idea of Naked Objects.

-- Richard Pawson

You can learn much more about the pattern in the book, Naked Objects, also freely available to read online. Richard co-wrote the book with one of Isis' committers, Robert Matthews, who was in turn the author of the Naked Objects Framework for Java (the original codebase of of Apache Isis).

You might also want to read Richard’s PhD on the subject.

One of the external examiners for Richard’s PhD was Trygve Reenskaug, who originally formulated the MVC pattern at Xeroc PARC. In his paper, Baby UML, Reenskaug describes that when implemented the first MVC, "the conventional wisdom in the group was that objects should be visible and tangible, thus bridging the gap between the human brain and the abstract data within the computer." Sound familiar? It’s interesting to speculate what might have been if this idea had been implemented back then in the late 70s.

Reenskaug then goes on to say that "this simple and powerful idea failed because …​ users were used to seeing [objects] from different perspectives. The visible and tangible object would get very complex if it should be able to show itself and be manipulated in many different ways."

In Apache Isis the responsibility of rendering an object is not the object itself, it is the framework. Rather, the object inspects the object and uses that to decide how to render the object. This is also extensible. In the Isis Addons (non-ASF) the Isis addons' gmap3 wicket extension renders any object with latitude/longitude on a map, while Isis addons' fullcalendar2 wicket extension renders any object with date(s) on a calendar.

Object Interface Mapping

Another — more technical — way to think about the naked objects pattern is as an object interface mapper, or OIM. We sometimes use this idea to explain naked objects to a bunch of developers.

Just as an ORM (such as DataNucleus or Hibernate) maps domain entities to a database, you can think of the naked objects pattern as representing the concept of mapping domain objects to a user interface.

This is the way that the MetaWidget team, in particular Richard Kennard, the primary contributor, likes to describe their tool. MetaWidget has a number of ideas in common with Apache Isis, specifically the runtime generation of a UI for domain objects. You can hear more from Kennard and others on this Javascript Jabber podcast.

We compare Apache Isis' with MetaWidget here.

What this means in practice

The screencast below shows what all of this means in practice, showing the relationship between a running app and the actual code underneath.

How Apache Isis builds a webapp from the underlying domain object model…

This screencast shows Apache Isis v1.0.0, Jan 2013. The UI has been substantially refined since that release.

1.1.3. Hexagonal Architecture

One of the patterns that Evans discusses in his book is that of a layered architecture. In it he describes why the domain model lives in its own layer within the architecture. The other layers of the application (usually presentation, application, and persistence) have their own responsibilities, and are completely separate. Each layer is cohesive and depending only on the layers below. In particular, we have a layer dedicated to the domain model. The code in this layer is unencumbered with the (mostly technical) responsibilities of the other layers and so can evolve to tackle complex domains as well as simple ones.

This is a well-established pattern, almost a de-facto; there’s very little debate that these responsibilities should be kept separate from each other. With Apache Isis the responsibility for presentation is a framework concern, the responsibility for the domain logic is implemented by the (your) application code.

A few years ago Alistair Cockburn reworked the traditional layered architecture diagram and came up with the hexagonal architecture:.

The hexagonal architecture is also known as the Ports and Adapters architecture or (less frequently) as the Onion architecture.

hexagonal architecture
Figure 1. The hexagonal architecture emphasizes multiple implementations of the different layers.

What Cockburn is emphasizing is that there’s usually more than one way into an application (what he called the user-side' ports) and more than one way out of an application too (the data-side ports). This is very similar to the concept of primary and secondary actors in use cases: a primary actor (often a human user but not always) is active and initiates an interaction, while a secondary actor (almost always an external system) is passive and waits to be interacted with.

Associated with each port can be an adapter (in fact, Cockburn’s alternative name for this architecture is ports and adapters). An adapter is a device (piece of software) that talks in the protocol (or API) of the port. Each port could have several adapters.

Apache Isis maps very nicely onto the hexagonal architecture. Isis' viewers act as user-side adapters and use the Isis metamodel API as a port into the domain objects. For the data side, we are mostly concerned with persisting domain objects to some sort of object store. Here Apache Isis delegates most of the heavy lifting to ORM implementing the JDO API. Most of the time this will be DataNucleus configured to persist to an RDBMS, but DataNucleus can also support other object stores, for example Neo4J. Alternatively Isis can be configured to persist using some other JDO implementation, for example Google App Engine.

1.1.4. Aspect Oriented

Although not a book about object modelling, Evans' "Domain Driven Design" does use object orientation as its primary modelling tool; and many tend to think of in the same vein as Rebecca Wirf Brock’s Responsibility Driven Design, say. Meanwhile the <<_naked_objects_pattern, naked objects pattern very much comes from an OO background (it even has 'object' in its name); and Richard Pawson lists Alan Kay as a key influence.

It’s certainly true that to develop an Apache Isis application you will need to have good object oriented modelling skills. But given that all the mainstream languages for developing business systems are object oriented (Java, C#, Ruby), that’s not such a stretch.

However, what you’ll also find as you write your applications is that in some ways an Isis application is more aspect-oriented than it is object oriented. Given that aspect-orientation — as a programming paradigm at least — hasn’t caught on, that statement probably needs unpacking a little.

AOP Concepts

Aspect-orientation, then, is a different way of decomposing your application, by treating cross-cutting concerns as a first-class citizen. The canonical (also rather boring) example of a cross-cutting concern is that of logging (or tracing) all method calls. An aspect can be written that will weave in some code (a logging statement) at specified points in the code).

This idea sounds rather abstract, but what it really amounts to is the idea of interceptors. When one method calls another the AOP code is called in first. This is actually then one bit of AOP that is quite mainstream; DI containers such as Spring provide aspect orientation in supporting annotations such as @Transactional or @Secured to java beans.

Another aspect (ahem!) of aspect-oriented programming has found its way into other programming languages, that of a mix-in or trait. In languages such as Scala these mix-ins are specified statically as part of the inheritance hierarchy, whereas with AOP the binding of a trait to some other class/type is done without the class "knowing" that additional behaviour is being mixed-in to it.

Realization within Isis

What has all this to do with Apache Isis, then?

Well, a different way to think of the naked objects pattern is that the visualization of a domain object within a UI is a cross-cutting concern. By following certain very standard programming conventions that represent the Apache Isis Programming Model (POJOs plus annotations), the framework is able to build a metamodel and from this can render your domain objects in a standard generic fashion. That’s a rather more interesting cross-cutting concern than boring old logging!

Isis also draws heavily on the AOP concept of interceptors. Whenever an object is rendered in the UI, it is filtered with respect to the user’s permissions. That is, if a user is not authorized to either view or perhaps modify an object, then this is applied transparently by the framework. The Isis addons' security module, mentioned previously, provides a rich user/role/permissions subdomain to use out of the box; but you can integrate with a different security mechanism if you have one already.

Another example of interceptors are the Isis addons' command and Isis addons' audit modules. The command module captures every user interaction that modifies the state of the system (the "cause" of a change) while the audit module captures every change to every object (the "effect" of a change). Again, this is all transparent to the user.

Apache Isis also has an internal event bus (you can switch between an underlying implementation of Gauva or Axon). A domain event is fired whenever an object is interacted with, and this allows any subscribers to influence the operation (or even veto it). This is a key mechanism in ensuring that Isis applications are maintainable, and we discuss it in depth in the section on Decoupling. But fundamentally its relying on this AOP concept of interceptors.

Finally, Isis also a feature that is akin to AOP mix-ins. A "contributed action" is one that is implemented on a domain service but that appears to be a behaviour of rendered domain object. In other words, we can dissociate behaviour from data. That’s not always the right thing to do of course. In Richard Pawson’s description of the naked objects pattern he talks about "behaviourally rich" objects, in other words where the business functionality encapsulated the data. But on the other hand sometimes the behaviour and data structures change at different rates. The single responsibility principle says we should only lump code together that changes at the same rate. Isis' support for contributions (not only contributed actions, but also contributed properties and contributed collections) enables this. And again, to loop back to the topic of this section, it’s an AOP concept that being implemented by the framework.

The nice thing about aspect orientation is that for the most part you can ignore these cross-cutting concerns and - at least initially at least - just focus on implementing your domain object. Later when your app starts to grow and you start to break it out into smaller modules, you can leverage Isis' AOP support for mixins (contributions) and interceptors (the event bus) to ensure that your codebase remains maintainable.

1.1.5. How Apache Isis eases DDD

The case for DDD might be compelling, but that doesn’t necessarily make it easy to do. Let’s take a look at some of the challenges that DDD throws up and see how Apache Isis (and its implementation of the naked objects pattern) helps address them.

DDD takes a conscious effort

Here’s what Eric Evans says about ubiquitous language:

With a conscious effort by the [development] team the domain model can provide the backbone for [the] common [ubiquitous] language…​connecting team communication to the software implementation."

-- Eric Evans

The word to pick up on here is conscious. It takes a conscious effort by the entire team to develop the ubiquitous language. Everyone in the team must challenge the use of new or unfamiliar terms, must clarify concepts when used in a new context, and in general must be on the lookout for sloppy thinking. This takes willingness on the part of all involved, not to mention some practice.

With Apache Isis, though, the ubiquitous language evolves with scarcely any effort at all. For the business experts, the Isis viewers show the domain concepts they identify and the relationships between those concepts in a straightforward fashion. Meanwhile, the developers can devote themselves to encoding those domain concepts directly as domain classes. There’s no technology to get distracted by; there is literally nothing else for the developers to work on.

DDD must be grounded

Employing a model-driven design isn’t necessarily straightforward, and the development processes used by some organizations positively hinder it. It’s not sufficient for the business analysts or architects to come up with some idealized representation of the business domain and then chuck it over the wall for the programmers to do their best with.

Instead, the concepts in the model must have a very literal representation in code. If we fail to do this, then we open up the communication divide, and our ubiquitous language is lost. There is literally no point having a domain model that cannot be represented in code. We cannot invent our ubiquitous language in a vacuum, and the developers must ensure that the model remains grounded in the doable.

In Apache Isis, we have a very pure one-to-one correspondence between the domain concepts and its implementation. Domain concepts are represented as classes and interfaces, easily demonstrated back to the business. If the model is clumsy, then the application will be clumsy too, and so the team can work together to find a better implementable model.

Abstract models are difficult to represent

If we are using code as the primary means of expressing the model, then we need to find a way to make this model understandable to the business.

We could generate UML diagrams and the like from code. That will work for some members of the business community, but not for everyone. Or we could generate a PDF document from Javadoc comments, but comments aren’t code and so the document may be inaccurate. Anyway, even if we do create such a document, not everyone will read it.

A better way to represent the model is to show it in action as a working prototype. As we show in the Getting Started section, Apache Isis enables this with ease. Such prototypes bring the domain model to life, engaging the audience in a way that a piece of paper never can.

Moreover, with Apache Isis prototypes, the domain model will come shining through. If there are mistakes or misunderstandings in the domain model (inevitable when building any complex system), they will be obvious to all.

Layered architectures are easily compromised

DDD rightly requires that the domain model lives in its own layer within the architecture. The other layers of the application (usually presentation, application, and persistence) have their own responsibilities, and are completely separate.

However, there are two immediate issues. The first is rather obvious: custom coding each of those other layers is an expensive proposition. Picking up on the previous point, this in itself can put the kibosh on using prototyping to represent the model, even if we wanted to do so.

The second issue is more subtle. It takes real skill to ensure the correct separation of concerns between these layers, if indeed you can get an agreement as to what those concerns actually are. Even with the best intentions, it’s all too easy for custom-written layers to blur the boundaries and put (for example) validation in the user interface layer when it should belong to the domain layer. At the other extreme, it’s quite possible for custom layers to distort or completely subvert the underlying domain model.

Because of Apache Isis' generic OOUIs, there’s no need to write the other layers of the architecture. Of course, this reduces the development cost. But more than that, there will be no leakage of concerns outside the domain model. All the validation logic must be in the domain model because there is nowhere else to put it.

Moreover, although Apache Isis does provide a complete runtime framework, there is no direct coupling of your domain model to the framework. That means it is very possible to take your domain model prototyped in Naked Objects and then deploy it on some other J(2)EE architecture, with a custom UI if you want. Isis guarantees that your domain model is complete.

Extending the reach of DDD

Domain-driven design is often positioned as being applicable only to complex domains; indeed, the subtitle of Evans book is "Tackling Complexity in the Heart of Software". The corollary is that DDD is overkill for simpler domains. The trouble is that we immediately have to make a choice: is the domain complex enough to warrant a domain-driven approach?

This goes back to the previous point, building and maintaining a layered architecture. It doesn’t seem cost effective to go to all the effort of a DDD approach if the underlying domain is simple.

However, with Apache Isis, we don’t write these other layers, so we don’t have to make a call on how complex our domain is. We can start working solely on our domain, even if we suspect it will be simple. If it is indeed a simple domain, then there’s no hardship, but if unexpected subtleties arise, then we’re in a good position to handle them.

If you’re just starting out writing domain-driven applications, then Apache Isis should significantly ease your journey into applying DDD. On the other hand, if you’ve used DDD for a while, then you should find Isis a very useful new tool in your arsenal.

1.2. Principles and Values

This section describes some of the core principles and values that the framework aims to honour and support.

The first of these relate to how we believe your domain application should be written: it should be decoupled, testable and so on). Others relate to the implementation of the framework itself.

The section concludes by contrasting the framework with some other open source frameworks commonly used.

1.2.1. Your Applications

TODO
Decoupled
TODO

Long-term Cost of ownership

Using:

  • dependency injection of services

  • OO design techniques, eg dependency inversion principle

  • an in-memory event bus

  • applib

  • (no "Big Ball of Mud")

Honouring the Single Responsibility Principle
TODO - behaviourally Complete vs Contributions
Testable
WIP

While Apache Isis can be used (very effectively) for simple CRUD-style applications, it is also intended to be used for complex business domains. Ensuring that the business logic in such applications is correct means that the framework must (and does) provide robust testing support, both for developer-level unit testing and business-level (end-to-end) integration testing.

1.2.2. Isis itself

This section discusses some of the principles and values we apply to the development of the Apache Isis framework itself.

Full-stack but Extensible
TODO
Focuses on its USP
TODO

add-ons

  • Isis is at heart a metamodel with runtime, and coordinates interations using an AOP set of principles

  • Apache Isis vs Isis Addons

  • Apache Isis vs Shiro vs DataNucleus

    1. all code has legacy in it…​. parts of the Isis codebase are well over a decade old; and back then a lot of the JEE technologies that we’d like to be using just didn’t exist, so we had to invent the features we required ourselves.

    2. also, Apache Isis today is more pragmatic than previously

  • a client/server solution, with AWT-based client

  • a HTML browser, Scimpi (JSF-like, but not using JSF), …​

  • security

  • objectstores

We’re working hard to remove duplication, reuse existing open source/JEE, and simplify.

The areas of Isis we consider mature are those that have been developed in support of real-world applications implemented by the committers. Foremost among these is Estatio.

Focus on enterprise / line-of-business applications, for use by internal staff.

  • problem solvers, not process followers

  • view models

1.2.3. Apache Isis vs …​

Many other frameworks promise rapid application development and provide automatically generated user interfaces, so how do they compare to Apache Isis?

vs MVC server-side frameworks

Some of most commonly used frameworks today are Spring MVC, Ruby on Rails and Grails, all of which implement one flavour or another of the server-side MVC pattern. The MVC 1.0 specification (scheduled for JavaEE 8) is also similar.

These frameworks all use the classic model-view-controller ( MVC) pattern for web applications, with scaffolding, code-generation, and/or metaprogramming tools for the controllers and views, as well as convention over configuration to define how these components interact. The views provided out of the box by these frameworks tend to be simple CRUD-style interfaces. More sophisticated behavior is accomplished by customizing the generated controllers.

The most obvious difference when developing an Apache Isis application is its deliberate lack of an explicit controller layer; non- CRUD behavior is automatically made available in its generic object-oriented _UI_s. More sophisticated UIs can be built either by extending Isis' Wicket viewer or by writing a bespoke UI leveraging the REST (hypermedia) API automatically exposed by Isis' Restful Objects viewer. Other frameworks can also be used to implement REST APIs, of course, but generally they require a significant amount of development to get anywhere near the level of sophistication provided automatically by Isis' REST API.

Although these frameworks all provide their own ecosystems of extensions, Isis' equivalent Isis Addons (non-ASF) tend to work at a higher-level of abstraction. For example, each of these frameworks will integrate with various security mechanism, but the Isis addons' security module provides a full subdomain of users, roles, features and permissions that can be plugged into any Isis application. Similarly, the Isis addons' command and Isis addons' audit modules in combination provide a support for auditing and traceability that can also be used for out of the box profiling. Again, these addons can be plugged into any Isis app.

In terms of testing support, each of these other frameworks provide mechanisms to allow the webapp to be tested from within a JUnit test harness. Isis' support is similar. Where Isis differs though is that it enables end-to-end testing without the need for slow and fragile Selenium tests. Instead, Isis provides a "WrapperFactory" domain service that allows the generic UI provided to in essence be simulated. On a more pragmatic level, the Isis addons' fakedata module does "what it says on the tin", allowing both unit- and integration-tests to focus on the salient data and fake out the rest.

vs CQRS
TODO
vs Event Sourcing
TODO
vs Angular
TODO
vs MetaWidget

MetaWidget (mentioned earlier has a number of ideas in common with Apache Isis, specifically the runtime generation of a UI for domain objects. And like Apache Isis, MetaWidget builds its own metamodel of the domain objects and uses this to render the object.

However, there is a difference in philosophy in that MW is not a full-stack framework and does not (in their words) try to "own the UI". Rather they support a huge variety of UI technologies and allow the domain object to be rendered in any of them.

In contrast, Apache Isis is full-stack and does generate a complete UI; we then allow you to customize or extend this UI (as per the various Isis Addons (non-ASF), and we also provide a full REST API through the Restful Objects viewer

Also, it’s worth noting that MetaWidget does have an elegant pipeline architecture, with APIs to allow even its metamodel to be replaced. It would be feasible and probably quite straightforward to use Isis' own metamodel as an implementation of the MetaWidget API. This would allow MetaWidget to be able to render an Isis domain application.

vs OpenXava
TODO

1.3. Building Blocks

TODO

1.3.1. A MetaModel with Explicit and Inferred Semantics

TODO
  • use of annotations

    • reuse JEE annotations where possible

  • layout hints are overridable, to avoid restarting the app/speed up feedback

1.3.2. Domain Objects and Domain Services

TODO
  • domain model

    • domain objects

      • domain entity

      • view model

    • domain service

    • value type

Anything else can be (should be) ignored by annotating with @Programmatic.

1.3.3. Properties, Collections and Actions

TODO

1.3.4. Domain Entities vs View Models

TODO

@DomainObject(nature=…​)

1.3.5. Domain Services

Domain services consist of a set of logically grouped actions, and as such follow the same conventions as for entities. However, a service cannot have (persisted) properties, nor can it have (persisted) collections.

Domain services are instantiated once and once only by the framework, and are used to centralize any domain logic that does not logically belong in a domain entity or value. Isis will automatically inject services into every domain entity that requests them, and into each other.

For convenience you can inherit from AbstractService or one of its subclasses, but this is not mandatory.

Domain Services vs View Services
TODO

@DomainService(nature=…​)

Factories, Repositories and Services

A distinction is sometimes made between a factory (that creates object) and a repository (that is used to find existing objects). You will find them discussed separately in Evans' Domain Driven Design, for example.

In Apache Isis these are all implemented as domain services. Indeed, it is quite common to have a domain service that acts as both a factory and a repository.

1.3.6. Value Objects (Primitives)

TODO

1.3.7. Contributions

TODO

1.3.8. Event Bus

TODO

1.4. Framework-provided Services

Most framework domain services are API: they exist to provide support functionality to the application’s domain objects and services. In this case an implementation of the service will be available, either by Apache Isis itself or by Isis Addons (non ASF).

Some framework domain services are SPI: they exist primarily so that the application can influence the framework’s behaviour. In these cases there is (usually) no default implementation; it is up to the application to provide an implementation.

General purpose:

Commands/Background/Auditing:

Information Sharing:

UserManagement:

Bookmarks and Mementos:

A full list of services can be found in the Domain Services (API) and Domain Services (SPI) chapters of the reference guide.

1.5. Isis Add-ons

The Isis Addons website provides a number of reusable modules and other extensions for Apache Isis. This chapter focuses just on the modules, all of which have a name of the form isis-module-xxx.

Note that Isis addons, while maintained by Isis committers, are not part of the ASF.

The modules themselves fall into four broad groups:

  • modules that provide an implementations of API defined by Apache Isis

    where Isis has hooks to use the service if defined by provides no implementations of its own. The command, auditing, publishing, security and sessionlogger modules fall into this category. Typically the domain objects themselves wouldn’t interact with these services

  • modules that provide standalone domain services with their own API and implementation

    These are simply intended to be used by domain objects. The docx, excel, settings and stringinterpolator fall into this category.

  • modules that provide standalone domain entities (and supporting services) for a particular subdomain

    The tags module falls into this category

  • modules that provide developer utilities

    Not intended for use by either the framework or domain objects, but provide utilities that the developer themselves might use. The devutils module (not suprisingly) falls into this category

Each of the modules has a full README and example application demonstrating their usage. The sections below briefly outline the capabilities of these modules.

1.6. Other Deployment Options

Apache Isis is a mature platform suitable for production deployment, with its "sweet spot" being line-of-business enterprise applications. So if you’re looking to develop that sort of application, we certainly hope you’ll seriously evaluate it.

But there are other ways that you can make Isis work for you; in this chapter we explore a few of them.

1.6.1. Deploy to production

Let’s start though with the default use case for Isis: building line-of-business enterprise applications, on top of its Wicket viewer.

Apache Wicket, and therefore Apache Isis in this configuration, is a stateful architecture. As a platform it is certainly capable of supporting user bases of several thousand (with perhaps one or two hundred concurrent); however it isn’t an architecture that you should try to scale up to tens of thousands of concurrent users.

The UI UI generated by the Wicket viewer is well suited to many line-of-business apps, but it’s also worth knowing that (with a little knowledge of the Wicket APIs) it relatively straightforward to extend. As described in Isis addons chapter, the viewer already has integrations with google maps, a full calendar and an export to Excel component. We are also aware of integrations with SVG images (for floor maps of shopping center) and of custom widgets displaying a catalogue (text and images) of medical diseases.

Deploying on Isis means that the framework also manages object persistence. For many line-of-business applications this will mean using a relational database. It is also possible (courtesy of its integratinon with DataNucleus) to deploy an Isis app to a NoSQL store such as Neo4J or MongoDB; and it is also possible to deploy to cloud platforms such as Google App Engine (GAE).

1.6.2. Use for prototyping

Even if you don’t intend to deploy your application on top of Isis, there can be a lot of value in using Isis for prototyping. Because all you need do to get an app running is write domain objects, you can very quickly explore a domain object model and validate ideas with a domain expert.

By focusing just on the domain, you’ll also find that you start to develop a ubiquitous language - a set of terms and concepts that the entire teamEnum (business and technologists alike) have a shared understanding.

Once you’ve sketched out your domain model, you can then "start-over" using your preferred platform.

1.6.3. Deploy on your own platform

The programming model defined by Isis deliberately minimizes the dependencies on the rest of the framework. In fact, the only hard dependency that the domain model classes have on Isis is through the org.apache.isis.applib classes, mostly to pick up annotations such as @Disabled. So, if you have used Isis for prototyping (discussed above), then note that it’s quite feasible to take your domain model a the basis of your actual development effort; Isis' annotations and programming conventions will help ensure that any subtle semantics you might have captured in your prototyping are not lost.

If you go this route, your deployment platform will of course need to provide similar capabilities to Isis. In particular, you’ll need to figure out a way to inject domain services into domain entities (eg using a JPA listener), and you’ll also need to reimplement any domain services you have used that Isis provides "out-of-the-box" (eg QueryResultsCache domain service).

1.6.4. Deploy the REST API

REST (Representation State Transfer) is an architectural style for building highly scalable distributed systems, using the same principles as the World Wide Web. Many commercial web APIs (twitter, facebook, Amazon) are implemented as either pure REST APIs or some approximation therein.

The Restful Objects specification defines a means by a domain object model can be exposed as RESTful resources using JSON representations over HTTP. Isis' <<_restfulobjects_viewer, Restful Objects viewer] is an implementation of that spec, making any Isis domain object automatically available via REST.

There are a number of use cases for deploying Isis as a REST API, including:

  • to allow a custom UI to be built against the RESTful API

    For example, using AngularJS or some other RIA technology such as Flex, JavaFX, Silverlight

  • to enable integration between systems

    REST is designed to be machine-readable, and so is an excellent choice for synchronous data interchange scenarios.

  • as a ready-made API for migrating data from one legacy system to its replacement.

As for the auto-generated webapps, the framework manages object persistence. It is perfectly possible to deploy the REST API alongside an auto-generated webapp; both work from the same domain object model.

1.6.5. Implement your own viewer

Isis' architecture was always designed to support multiple viewers; and indeed Isis out-of-the-box supports two: the Wicket viewer, and the Restful Objects viewer (or three, if one includes the Wrapper Factory).

While we mustn’t understate the effort involved here, it is feasible to implement your own viewers too. Indeed, one of Isis' committers does indeed have a (closed source) viewer, based on Wavemaker.

2. Getting Started

To get you up and running quickly, Apache Isis provides a SimpleApp archetype to setup a simple application as the basis of your own apps. This is deliberately very minimal so that you won’t have to spend lots of time removing generated artifacts. On the other hand, it does set up a standard multi-module maven structure with unit- and integration tests pre-configured, as well as a webapp module so that you can easily run your app. We strongly recommend that you preserve this structure as you develop your own Isis application.

In this chapter we also discuss the DataNucleus enhancer. DataNucleus is the reference implementation of the JDO (Java data objects) spec, and Isis integrates with DataNucleus as its persistence layer. The enhancer performs post-processing on the bytecode of your persistent domain entities, such that they can be persisted by Isis' JDO/DataNucleus objectstore.

The SimpleApp archetype automatically configures the enhancer, so there’s little you need to do at this stage. Even so we feel it’s a good idea to be aware of this critical part of Isis runtime; if the enhancer does not run, then you’ll find the app fails to start with (what will seem like) quite an obscure exception message.

2.1. Prerequisites

Apache Isis is a Java based framework, so in terms of prerequisites, you’ll need to install:

You’ll probably also want to use an IDE; the Isis committers use either IntelliJ or Eclipse; in the appendices we have detailed setup instructions for using these two IDEs. If you’re a NetBeans user you should have no problems as it too has strong support for Maven.

When building and running within an IDE, you’ll also need to configure the Datanucleus enhancer. This is implemented as a Maven plugin, so in the case of IntelliJ, it’s easy enough to run the enhancer as required. It should be just as straightforward for NetBeans too.

For Eclipse the maven integration story is a little less refined. All is not lost, however; DataNucleus also has an implementation of the enhancer as an Eclipse plugin, which usually works well enough.

2.2. SimpleApp Archetype

The quickest way to get started with Apache Isis is to run the simple archetype. This will generate a very simple one-class domain model, called SimpleObject, with a single property name.

There is also a corresponding SimpleObjectRepository domain service. From this you can easily rename these initial classes, and extend to build up your own Isis domain application.

2.2.1. Generating the App

Create a new directory, and cd into that directory.

To build the app from the latest stable release, then run the following command:

mvn archetype:generate  \
    -D archetypeGroupId=org.apache.isis.archetype \
    -D archetypeArtifactId=simpleapp-archetype \
    -D archetypeVersion=1.8.0 \
    -D groupId=com.mycompany \
    -D artifactId=myapp \
    -D version=1.0-SNAPSHOT \
    -B

where:

  • groupId represents your own organization, and

  • artifactId is a unique identifier for this app within your organization.

  • version is the initial (snapshot) version of your app

The archetype generation process will then run; it only takes a few seconds.

We also maintain the archetype for the most current -SNAPSHOT; an app generated with this archetype will contain the latest features of Isis, but the usual caveats apply: some features still in development may be unstable.

The process is almost identical to that for stable releases, however the archetype:generate goal is called with slightly different arguments:

mvn archetype:generate  \
    -D archetypeGroupId=org.apache.isis.archetype \
    -D archetypeArtifactId=simpleapp-archetype \
    -D archetypeVersion=1.9.0-SNAPSHOT \
    -D groupId=com.mycompany \
    -D artifactId=myapp \
    -D version=1.0-SNAPSHOT \
    -D archetypeRepository=http://repository-estatio.forge.cloudbees.com/snapshot/ \
    -B

where as before:

  • groupId represents your own organization, and

  • artifactId is a unique identifier for this app within your organization.

  • version is the initial (snapshot) version of your app

but also:

  • archetypeVersion is the SNAPSHOT version of Isis.

  • archetypeRepository specifies the location of our snapshot repo (hosted on CloudBees), and

The archetype generation process will then run; it only takes a few seconds.

2.2.2. Building the App

Switch into the root directory of your newly generated app, and build your app:

cd myapp
mvn clean install

where myapp is the artifactId entered above.

2.2.3. Running the App

The simpleapp archetype generates a single WAR file, configured to run both the Wicket viewer and the Restful Objects viewer. The archetype also configures the [JDO Objectstore](../../components/objectstores/jdo/about.html) to use an in-memory HSQLDB connection.

Once you’ve built the app, you can run the WAR in a variety of ways.

Self-hosted

The easiest way to run the app, at least while getting started, is to run the self-hosting version of the WAR. With this style the app runs up its own internal instance of Jetty web server.

For example:

java -jar webapp/target/myapp-webapp-1.0-SNAPSHOT-jetty-console.jar

This can also be accomplished using an embedded Ant target provided in the build script:

mvn -P self-host antrun:run
Using mvn Jetty plugin

Alternatively, you could run the WAR in a Maven-hosted Jetty instance, though you need to cd into the webapp module:

cd webapp
mvn jetty:run

If you use mvn jetty:run, then the context path changes; check the console output (eg link:[http://localhost:8080/myapp-webapp] [http://localhost:8080/myapp-webapp]).

You can also provide a system property to change the port:

cd webapp
mvn jetty:run -D jetty.port=9090
Using a regular servlet container

You can also take the built WAR file and deploy it into a standalone servlet container such as [Tomcat](http://tomcat.apache.org). The default configuration does not require any configuration of the servlet container; just drop the WAR file into the webapps directory.

From within the IDE

Most of the time, though, you’ll probably want to run the app from within your IDE. The mechanics of doing this will vary by IDE; see the appendices for details on setting up Eclipse or setting up IntelliJ IDEA. Basically, though, it amounts to running org.apache.isis.WebServer, and ensuring that the DataNucleus enhancer has properly processed all domain entities.

Here’s what the setup looks like in IntelliJ IDEA:

simpleapp webapp

2.2.4. Running with Fixtures

It is also possible to start the application with a pre-defined set of data; useful for demos or manual exploratory testing. This is done by specifying a fixture script on the command line.

If running the self-hosted console, you can specify the fixtures using the --initParam flag:

java -jar webapp/target/myapp-webapp-1.0-SNAPSHOT-jetty-console.jar \
     --initParam isis.persistor.datanucleus.install-fixtures=true  \
     --initParam isis.fixtures=domainapp.fixture.scenarios.RecreateSimpleObjects

where (in the above example) domainapp.fixture.scenarios.RecreateSimpleObjects is the fully qualified class name of the fixture script to be run.

If you are running the app from an IDE, then you can specify the fixture script using the --fixture flag:

simpleapp webapp with fixtures

2.2.5. Using the App

When you start the app, you’ll be presented with a welcome page from which you can access the webapp using either the Wicket viewer or the Restful Objects viewer:

010 root page

The Wicket viewer provides a human usable web UI (implemented, as you might have guessed from its name, using Apache Wicket), so choose that and navigate to the login page:

020 login to wicket viewer

The app itself is configured to run using shiro security, as configured in the WEB-INF/shiro.ini config file. You can login with:

  • username: sven

  • password: pass

The application is configured to run with an in-memory database, and (unless you started the app with fixture scripts as described above), initially there is no data. We can though run a fixture script from the app itself:

030 home page run fixture scripts

The fixture script creates three objects, and the action returns the first of these:

040 first object

The application generated is deliberaetly very minimal; we don’t want you to have to waste valuable time removing generated files. The object contains a single "name" property, and a single action to update that property:

050 update name prompt

When you hit OK, the object is updated:

060 object updated

For your most signficant domain entities you’ll likely have a domain service to retrieve or create instances of those obejcts. In the generated app we have a "Simple Objects" domain service that lets us list all objects:

070 list all prompt

whereby we see the three objects created by the fixture script (one having been updated):

080 list all

and we can also use the domain service to create new instances:

090 create

prompting us for the mandatory information (the name):

100 create prompt

which, of course, returns the newly created object:

110 object created

When we list all objects again, we can see that the object was indeed created:

120 list all

Going back to the home page (localhost:8080) we can also access the Restful Objects viewer. The generated application is configured to use HTTP Basic Auth:

220 login to restful viewer

The Restful Objects viewer provides a REST API for computer-to-computer interaction, but we can still interact with it from a browser:

230 home page

Depending on your browser, you may need to install plugins. For Chrome, we recommend json-view (which renders the JSON indented and automatically detects hyperlinks) and REST Postman.

The REST API is a complete hypermedia API, in other words you can follow the links to access all the behaviour exposed in the regular Wicket app. For example, we can navigate to the listAll/invoke resource:

240 list all invoke

which when invoked (with an HTTP GET) will return a representation of the domain objects.

250 list all results

To log in, use sven/pass.

2.2.6. Modifying the App

Once you are familiar with the generated app, you’ll want to start modifying it. There is plenty of guidance on this site; check out the 'programming model how-tos' section on the main [documentation](../../documentation.html) page first).

If you use Eclipse or IntelliJ IDEA, do also install the [Eclipse templates](../resources/editor-templates.html); these will help you follow the Isis naming conventions.

2.2.7. App Structure

As noted above, the generated app is a very simple application consisting of a single domain object that can be easily renamed and extended. The intention is not to showcase all of Isis' capabilities; rather it is to allow you to very easily modify the generated application (eg rename SimpleObject to Customer) without having to waste time deleting lots of generated code.

<table class="table table-striped table-bordered table-condensed"> <tr><th>Module</th><th>Description</th></tr> <tr><td>myapp</td><td>The parent (aggregator) module</td></tr> <tr><td>myapp-dom</td><td>The domain object model, consisting of <tt>SimpleObject</tt> and <tt>SimpleObjects</tt> (repository) domain service.</td></tr> <tr><td>myapp-fixture</td><td>Domain object fixtures used for initializing the system when being demo’ed or for unit testing.</td></tr> <tr><td>myapp-integtests</td><td>End-to-end <a href="../../core/integtestsupport.html">integration tests</a>, that exercise from the UI through to the database</td></tr> <tr><td>myapp-webapp</td><td>Run as a webapp (from <tt>web.xml</tt>) using either the Wicket viewer or the Restful Objects viewer</td></tr> </table>

If you run into issues, please don’t hesitate to ask for help on the [users mailing list](../../support.html).

2.3. Datanucleus Enhancer

DataNucleus is the reference implementation of the JDO (Java data objects) spec, and Isis integrates with DataNucleus as its persistence layer. Datanucleus is a very powerful library, allowing domain entities to be mapped not only to relational database tables, but also to NoSQL stores such as Neo4J, MongoDB and Apache Cassandra.

With such power comes a little bit of complexity to the development environment: all domain entities must be enhanced through the DataNucleus enhancer.

Bytecode enhancement is actually a requirement of the JDO spec; the process is described in outline here.

What this means is that the enhancer — available as both a Maven plugin and as an Eclipse plugin — must, one way or another, be integrated into your development environment.

If working from the Maven command line, JDO enhancement is done using the maven-datanucleus-plugin. As of 1.9.0-SNAPSHOT, we put all the configuration into an (always active) profile:

The configuration described below is automatically set up by the SimpleApp archetype.

<profile>
    <id>enhance</id>
    <activation>
        <activeByDefault>true</activeByDefault>
    </activation>
    <properties>
        <datanucleus-maven-plugin.version>4.0.0-release</datanucleus-maven-plugin.version>
    </properties>
    <build>
        <plugins>
            <plugin>
                <groupId>org.datanucleus</groupId>
                <artifactId>datanucleus-maven-plugin</artifactId>
                <version>${datanucleus-maven-plugin.version}</version>
                <configuration>
                    <fork>false</fork>
                    <log4jConfiguration>${basedir}/log4j.properties</log4jConfiguration>
                    <verbose>true</verbose>
                    <props>${basedir}/datanucleus.properties</props>
                </configuration>
                <executions>
                    <execution>
                        <phase>process-classes</phase>
                        <goals>
                            <goal>enhance</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>org.datanucleus</groupId>
            <artifactId>datanucleus-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.datanucleus</groupId>
            <artifactId>datanucleus-jodatime</artifactId>
        </dependency>
        <dependency>
            <groupId>org.datanucleus</groupId>
            <artifactId>datanucleus-api-jdo</artifactId>
        </dependency>
    </dependencies>
</profile>

The SimpleApp archetype sets up the plugin correctly in the dom (domain object model) module. (It’s actually a little bit more complex to cater for users of the Eclipse IDE using Eclipse’s m2e plugin).

2.3.1. META-INF/persistence.xml

It’s also a good idea to ensure that the dom module has a JDO META-INF/persistence.xml file:

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="simple"> (1)
    </persistence-unit>
</persistence>
1 change as required; typically is the name of the app.

Again, the SimpleApp archetype does this.

If running on Windows, then there’s a good chance you’ll hit the maximum path length limit. In this case the persistence.xml file is mandatory rather than optional.

This file is also required if you are using developing in Eclipse and relying on the DataNucleus plugin for Eclipse rather than the DataNucleus plugin for Maven. More information can be found here.

3. Tutorials

This chapter contains a couple of tutorials for you to follow.

  • the "petclinic" tutorial takes you step-by-step through building a simple application of just three classes. There are example solutions in the github repo in case you get lost.

  • the "stop scaffolding, start coding" tutorial is taken from a conference workshop. It has less hand-holding, but lists out the steps for you to follow. It’s a good cookbook to follow when you’re readng to take things further.

Have fun!

3.1. Pet Clinic

This is a step-by-step tutorial to build up a simple "petclinic" application, starting from the SimpleApp archetype.

It consists of just three domain classes (http://yuml.me/3db2078c):

domain model

This supports the following use cases:

  • register a Pet

  • register an Owner

  • maintain a Pet’s details

  • check in a Pet to visit the clinic

  • enter a diagnosis

check out a Pet to visit the clinic

Either follow along or check out the tags from the corresponding github repo.

3.1.1. Prerequisites

You’ll need:

3.1.2. Run the archetype

Throughout this tutorial you can, if you wish, just checkout from the github repo wherever you see a "git checkout" note:

git checkout https://github.com/danhaywood/isis-app-petclinic/commit/249abe476797438d83faa12ff88365da2c362451

This tutorial was developed against Apache Isis 1.8.0-SNAPSHOT. Since then 1.8.0 has been released, so simply replace "1.8.0-SNAPSHOT" for "1.8.0" wherever it appears in the pom.xml files.

As per the Isis website, run the simpleapp archetype to build an empty Isis application:

mvn archetype:generate  \
    -D archetypeGroupId=org.apache.isis.archetype \
    -D archetypeArtifactId=simpleapp-archetype \
    -D archetypeVersion=1.8.0 \
    -D groupId=com.mycompany \
    -D artifactId=petclinic \
    -D version=1.0-SNAPSHOT \
    -D archetypeRepository=http://repository-estatio.forge.cloudbees.com/snapshot/ \
    -B

This will generate the app in a petclinic directory. Move the contents back:

mv petclinic/* .
rmdir petclinic

3.1.3. Build and run

Start off by building the app from the command line:

mvn clean install

Once that’s built then run using:

mvn antrun:run -P self-host

A splash screen should appear offering to start up the app. Go ahead and start; the web browser should be opened at http://localhost:8080

Alternatively, you can run using the mvn-jetty-plugin:

mvn jetty:run

This will accomplish the same thing, though the webapp is mounted at a slightly different URL

3.1.4. Using the app

Navigate to the Wicket UI (eg http://localhost:8080/wicket), and login (sven/pass).

010 01 login page

The home page should be shown:

010 02 home page

Install the fixtures (example test data) using the Prototyping menu:

010 03 prototyping menu

List all objects using the Simple Objects menu:

010 04 simpleobjects

To return the objects created:

010 05 simpleobject list

Experiment some more, to:

  • create a new object

  • list all objects

Go back to the splash screen, and quit the app. Note that the database runs in-memory (using HSQLDB) so any data created will be lost between runs.

3.1.5. Dev environment

Set up an IDE and import the project to be able to run and debug the app.

Then set up a launch configuration so that you can run the app from within the IDE. To save having to run the fixtures every time, specify the following system properties:

-Disis.persistor.datanucleus.install-fixtures=true -Disis.fixtures=fixture.simple.scenario.SimpleObjectsFixture

For example, here’s what a launch configuration in IntelliJ idea looks like:

020 01 idea configuration

where the "before launch" maven goal (to run the DataNucleus enhancer) is defined as:

020 02 idea configuration

3.1.6. Explore codebase

Apache Isis applications are organized into several Maven modules. Within your IDE navigate to the various classes and correlate back to the generated UI:

  • petclinic : parent module

  • petclinic-dom: domain objects module

    • entity: dom.simple.SimpleObject

    • repository: dom.simple.SimpleObjects

  • petclinic-fixture: fixtures module

    • fixture script:`fixture.simple.SimpleObjectsFixture`

  • petclinic-integtests: integration tests module

  • petclinic-webapp: webapp module

    • (builds the WAR file)

3.1.7. Testing

Testing is of course massively important, and Isis makes both unit testing and (end-to-end) integration testing easy. Building the app from the Maven command line ("mvn clean install") will run all tests, but you should also run the tests from within the IDE.

  • myapp-dom unit tests

  • run

  • inspect, eg

    • SimpleObjectTest

  • myapp-integtests integration tests

  • run

  • inspect, eg:

    • integration.tests.smoke.SimpleObjectsTest

    • integration.specs.simple.SimpleObjectSpec_listAllAndCreate.feature

  • generated report, eg

    • myapp/integtests/target/cucumber-html-report/index.html

      • change test in IDE, re-run (in Maven)

If you have issues with the integration tests, make sure that the domain classes have been enhanced by the DataNucleus enhancer. (The exact mechanics depends on the IDE being used).

3.1.8. Update POM files

git checkout https://github.com/danhaywood/isis-app-petclinic/commit/68904752bc2de9ebb3c853b79236df2b3ad2c944

The POM files generated by the simpleapp archetype describe the app as "SimpleApp". Update them to say "PetClinic" instead.

3.1.9. Delete the BDD specs

git checkout https://github.com/danhaywood/isis-app-petclinic/commit/9046226249429b269325dfa2baccf03635841c20

During this tutorial we’re going to keep the integration tests in-sync with the code, but we’re going to stop short of writing BDD/Cucumber specs.

Therefore delete the BDD feature spec and glue in the integtest module:

  • integration/specs/*

  • integration/glue/*

3.1.10. Rename artifacts

git checkout https://github.com/danhaywood/isis-app-petclinic/commit/bee3629c0b64058f939b6dd20f226be31810fc66

Time to start refactoring the app. The heart of the PetClinic app is the Pet concept, so go through the code and refactor. While we’re at it, refactor the app itself from "SimpleApp" to "PetClinicApp".

See the git commit for more detail, but in outline, the renames required are:

  • in the dom module’s production code

    • SimpleObject -> Pet (entity)

    • SimpleObjects -> Pets (repository domain service)

    • SimpleObject.layout.json -> Pet.layout.json (layout hints for the Pet entity)

    • delete the SimpleObject.png, and add a new Pet.png (icon shown against all Pet instances).

  • in the dom module’s unit test code

    • SimpleObjectTest -> PetTest (unit tests for Pet entity)

    • SimpleObjectsTest -> PetsTest (unit tests for Pets domain service)

  • in the fixture module:

    • SimpleObjectsFixturesService -> PetClinicAppFixturesService (rendered as the prototyping menu in the UI)

    • SimpleObjectsTearDownService -> PetClinicAppTearDownService (tear down all objects between integration tests)

    • SimpleObjectAbstract -> PetAbstract (abstract class for setting up a single pet object

      • and corresponding subclasses to set up sample data (eg PetForFido)

    • SimpleObjectsFixture -> PetsFixture (tear downs system and then sets up all pets)

  • in the integtest module:

    • SimpleAppSystemInitializer -> PetClinicAppSystemInitializer (bootstraps integration tests with domain service/repositories)

    • SimpleAppIntegTest -> PetClinicAppIntegTest (base class for integration tests)

    • SimpleObjectTest -> PetTest (integration test for Pet entity)

    • SimpleObjectsTest -> PetsTest (integration test for Pets domain service)

  • in the webapp module:

    • SimpleApplication -> PetClinicApplication

    • update isis.properties

    • update web.xml

Note that Pet has both both Isis and JDO annotations:

@javax.jdo.annotations.PersistenceCapable(identityType=IdentityType.DATASTORE) (1)
@javax.jdo.annotations.DatastoreIdentity(                                      (2)
        strategy=javax.jdo.annotations.IdGeneratorStrategy.IDENTITY,
         column="id")
@javax.jdo.annotations.Version(                                                (3)
        strategy=VersionStrategy.VERSION_NUMBER,
        column="version")
@javax.jdo.annotations.Unique(name="Pet_name_UNQ", members = {"name"})         (4)
@ObjectType("PET")                                                             (5)
@Bookmarkable                                                                  (6)
public class Pet implements Comparable<Pet> {
    ...
}

where:

1 @PersistenceCapable and
2 @DatastoreIdentity specify a surrogate Id column to be used as the primary key
3 @Version provides support for optimistic locking
4 @Unique enforces a uniqueness constraint so that no two `Pet`s can have the same name (unrealistic, but can refactor later)
5 @ObjectType is used by Isis for its own internal "OID" identifier; this also appears in the URL in Isis' Wicket viewer and REST API
6 @Bookmarkable indicates that the object can be automatically bookmarked in Isis' Wicket viewer

The @ObjectType and @Bookmarkable annotations have since been deprecated, replaced with @DomainObject(objectType=…​) and @DomainObjectLayout(bookmarking=…​)

The Pets domain service also has Isis annotations:

@DomainService(repositoryFor = Pet.class)
@DomainServiceLayout(menuOrder = "10")
public class Pets {
    ...
}

where:

  • DomainService indicates that the service should be instantiated automatically (as a singleton)

  • DomainServiceLayout provides UI hints, in this case the positioning of the menu for the actions provided by the service

To run the application will require an update to the IDE configuration, for the changed name of the fixture class:

030 01 idea configuration updated

Running the app should now show `Pet`s:

030 02 updated app

3.1.11. Update package names

git checkout https://github.com/danhaywood/isis-app-petclinic/commit/55ec36e520191f5fc8fe7f5b89956814eaf13317

The classes created by the simpleapp archetype are by default in the simple package. Move these classes to pets package instead. Also adjust package names where they appear as strings:

  • in PetClinicAppFixturesService, change the package name from "fixture.simple" to "fixture.pets".

  • in PetClinicAppSystemInitializer, change the package name "dom.simple" to "dom.pets", and similarly "fixture.simple" to "fixture.pets"

  • in WEB-INF/isis.properties, similarly change the package name "dom.simple" to "dom.pets", and similarly "fixture.simple" to "fixture.pets"

To run the application will require a further update to the IDE configuration, for the changed package of the fixture class:

040 01 idea configuration updated

3.1.12. Add PetSpecies enum

git checkout https://github.com/danhaywood/isis-app-petclinic/commit/55c9cd28ff960220719b3dc7cb8abadace8d0829

Each Pet is of a particular species. Model these as an enum called PetSpecies:

public enum PetSpecies {
    Cat,
    Dog,
    Budgie,
    Hamster,
    Tortoise
}

Introduce a new property on Pet of this type:

public class Pet {
    ...
    private PetSpecies species;
    @javax.jdo.annotations.Column(allowsNull = "false")
    public PetSpecies getSpecies() { return species; }
    public void setSpecies(final PetSpecies species) { this.species = species; }
    ...
}

Update fixtures, unit tests and integration tests.

3.1.13. Icon to reflect pet species

git checkout https://github.com/danhaywood/isis-app-petclinic/commit/2212765694693eb463f8fa88bab1bad154add0cb

Rather than using a single icon for a domain class, instead a different icon can be supplied for each instance. We can therefore have different icon files for each pet, reflecting that pet’s species.

public class Pet {
    ...
    public String iconName() {
        return getSpecies().name();
    }
    ...
}

Download corresponding icon files (Dog.png, Cat.png etc)

Running the app shows the Pet and its associated icon:

050 01 list all

with the corresponding view of the Pet:

050 02 view pet

3.1.14. Add pet’s Owner

git checkout https://github.com/danhaywood/isis-app-petclinic/commit/6f92a8ee8e76696d005da2a8b7a746444d017546

Add the Owner entity and corresponding Owners domain service (repository). Add a query to find `Order`s by name:

...
@javax.jdo.annotations.Queries( {
        @javax.jdo.annotations.Query(
                name = "findByName", language = "JDOQL",
                value = "SELECT "
                        + "FROM dom.owners.Owner "
                        + "WHERE name.matches(:name)")
})
public class Owner ... {
    ...
}

and findByName(…​) in Owners:

public class Owners {
    ...
    public List<Owner> findByName(
            @ParameterLayout(named = "Name")
            final String name) {
        final String nameArg = String.format(".*%s.*", name);
        final List<Owner> owners = container.allMatches(
                new QueryDefault<>(
                        Owner.class,
                        "findByName",
                        "name", nameArg));
        return owners;
    }
    ...
}

Add an owner property to Pet, with supporting autoCompleteXxx() method (so that available owners are shown in a drop-down list box):

public class Pet ... {
    ...
    private Owner owner;
    @javax.jdo.annotations.Column(allowsNull = "false")
    public Owner getOwner() { return owner; }
    public void setOwner(final Owner owner) { this.owner = owner; }
    public Collection<Owner> autoCompleteOwner(final @MinLength(1) String name) {
        return owners.findByName(name);
    }
    ...
}

Also updated fixture data to set up a number of Owner`s, and associate each `Pet with an Owner. Also add unit tests and integration tests for Owner/Owners and updated for Pet/Pets.

When running the app, notice the new Owners menu:

060 01 owners menu

which when invoked returns all Owner objects:

060 02 owners list

Each Pet also indicates its corresponding Owner:

060 03 pets list

And, on editing a Pet, a new Owner can be specified using the autoComplete:

060 04 pet owner autoComplete

3.2. Stop scaffolding, start coding

This is a half-day tutorial on developing domain-driven apps using Apache Isis. Actually, you could probably spend a full day working through this tutorial if you wanted to, so pick and choose the bits that look interesting.

There’s a bit of overlap with the Pet Clinic tutorial initially, but it then sets off on its own.

3.2.1. Prerequisites

You’ll need:

3.2.2. Run the archetype

As per the Isis website, run the simpleapp archetype to build an empty Isis application:

mvn archetype:generate  \
    -D archetypeGroupId=org.apache.isis.archetype \
    -D archetypeArtifactId=simpleapp-archetype \
    -D archetypeVersion=1.8.0 \
    -D groupId=com.mycompany \
    -D artifactId=myapp \
    -D version=1.0-SNAPSHOT \
    -D archetypeRepository=http://repository-estatio.forge.cloudbees.com/snapshot/ \
    -B

3.2.3. Build and run

Start off by building the app from the command line:

cd myapp
mvn clean install

Once that’s built then run using:

mvn antrun:run -P self-host

A splash screen should appear offering to start up the app. Go ahead and start; the web browser should be opened at http://localhost:8080

Alternatively, you can run using the mvn-jetty-plugin:

mvn jetty:run

This will accomplish the same thing, though the webapp is mounted at a slightly different URL

3.2.4. Using the app

Navigate to the Wicket UI (eg http://localhost:8080/wicket), and login (sven/pass).

Once at the home page:

  • install fixtures

  • list all objects

  • create a new object

  • list all objects

Go back to the splash screen, and quit the app. Note that the database runs in-memory (using HSQLDB) so any data created will be lost between runs.

3.2.5. Dev environment

Set up an IDE and import the project to be able to run and debug the app.

Then set up a launch configuration and check that you can:

  • Run the app from within the IDE

  • Run the app in debug mode

  • Run with different deploymentTypes; note whether prototype actions (those annotated @Action(restrictTo=PROTOTYPING) are available or not:

  • --type SERVER_PROTOTYPE

  • --type SERVER

3.2.6. Explore codebase

Apache Isis applications are organized into several Maven modules. Within your IDE navigate to the various classes and correlate back to the generated UI:

  • myapp : parent module

  • myapp-dom: domain objects module

  • entity: dom.simple.SimpleObject

  • repository: dom.simple.SimpleObjects

  • myapp-fixture: fixtures module

  • fixture script:`fixture.simple.SimpleObjectsFixture`

  • myapp-integtests: integration tests module

  • myapp-webapp: webapp module

  • (builds the WAR file)

3.2.7. Testing

Testing is of course massively important, and Isis makes both unit testing and (end-to-end) integration testing easy. Building the app from the Maven command line ("mvn clean install") will run all tests, but you should also run the tests from within the IDE.

  • myapp-dom unit tests

  • run

  • inspect, eg

    • SimpleObjectTest

  • myapp-integtests integration tests

  • run

  • inspect, eg:

    • integration.tests.smoke.SimpleObjectsTest

    • integration.specs.simple.SimpleObjectSpec_listAllAndCreate.feature

  • generated report, eg

    • myapp/integtests/target/cucumber-html-report/index.html

      • change test in IDE, re-run (in Maven)

If you have issues with the integration tests, make sure that the domain classes have been enhanced by the DataNucleus enhancer. (The exact mechanics depends on the IDE being used).

3.2.8. Prototyping

Although testing is important, in this tutorial we want to concentrate on how to write features and to iterate quickly. So for now, exclude the integtests module. Later on in the tutorial we’ll add the tests back in so you can learn how to write automated tests for the features of your app.

In the parent pom.xml:

<modules>
    <module>dom</module>
    <module>fixture</module>
    <module>integtests</module>
    <module>webapp</module>
</modules>

change to:

<modules>
    <module>dom</module>
    <module>fixture</module>
    <!--
    <module>integtests</module>
    -->
    <module>webapp</module>
</modules>

3.2.9. Build a domain app

The remainder of the tutorial provides guidance on building a domain application. We don’t mandate any particular design, but we suggest one with no more than 3 to 6 domain entities in the first instance. If you’re stuck for ideas, then how about:

  • a todo app (ToDoItems)

  • a pet clinic (Pet, Owner, PetSpecies, Visit)

  • a library (Book, Title, LibraryMember, Loan, Reservation)

  • a holiday cottage rental system

  • a scrum/kanban system (inspired by Trello)

  • a meeting planner (inspired by Doodle)

  • (the domain model for) a CI server (inspired by Travis/Jenkins)

  • a shipping system (inspired by the example in the DDD "blue" book)

  • a system for ordering coffee (inspired by Restbucks, the example in "Rest in Practice" book)

Hopefully one of those ideas appeals or sparks an idea for something of your own.

3.2.10. Domain entity

Most domain objects in Apache Isis applications are persistent entities. In the simpleapp archetype the SimpleObject is an example. We can start developing our app by refactoring that class:

  • rename the SimpleObject class

    • eg rename to Pet

  • if required, rename the SimpleObject class' name property

    • for Pet, can leave name property as is

  • specify a title

  • specify an icon

  • make the entity bookmarkable by adding the <<__code_domainobjectlayout_code,@DomainObjectLayout(bookmarking=…​)` annotation

  • confirm is available from bookmark panel (top-left of Wicket UI)

3.2.11. Domain service

Domain services often act as factories or repositories to entities; more generally can be used to "bridge across" to other domains/bounded contexts. Most are application-scoped, but they can also be request-scoped if required.

In the simpleapp archetype the SimpleObjects service is a factory/repository for the original SimpleObject entity. For our app it therefore makes sense to refactor that class into our own first service:

  • rename the SimpleObjects class

    • eg rename to Pets

  • review create action (acting as a factory)

  • rename if you wish

    • eg newPet(…​) or addPet(…​)

  • review listAll action (acting as a repository)

  • as per the docs describing how to write a custom repository

  • note the annotations on the corresponding domain class (originally called SimpleObject, though renamed by now, eg to Pet)

  • rename if you wish

    • eg listPets()

  • note the @DomainService annotation

  • optional: add an action to a return subset of objects

    • use the JDO @Query annotation

    • see for example the Isisaddons example todoapp (not ASF), see here and here

3.2.12. Fixture scripts

Fixture scripts are used to setup the app into a known state. They are great for demo’s and as a time-saver when implementing a feature, and they can also be reused in automated integration tests. We usually also have a fixture script to zap all the (non-reference) data (or some logical subset of the data)

  • rename the SimpleObjectsTearDownFixture class

  • and update to delete from the appropriate underlying database table(s)

  • use the injected IsisJdoSupport domain service.

  • refactor/rename the fixture script classes that create instances your entity:

  • RecreateSimpleObjects, which sets up a set of objects for a given scenario

  • SimpleObjectCreate which creates a single object

  • note that domain services can be injected into these fixture scripts

3.2.13. Actions

Most business functionality is implemented using actions� basically a public method accepting domain classes and primitives as its parameter types. The action can return a domain entity, or a collection of entities, or a primitive/String/value, or void. If a domain entity is returned then that object is rendered immediately; if a collection is returned then the Wicket viewer renders a table. Such collections are sometimes called "standalone" collections.

  • write an action to update the domain property (originally called SimpleObject#name, though renamed by now)

  • use the @ParameterLayout(named=…​) annotation to specify the name of action parameters

  • use the @Action(semanticsOf=…​) annotation to indicate the semantics of the action (safe/query-only, idempotent or non-idempotent)

  • annotate safe action as bookmarkable using @ActionLayout(bookmarking=…​)

  • confirm is available from bookmark panel (top-left of Wicket UI)

  • optional: add an action to clone an object

3.2.14. REST API

As well as exposing the Wicket viewer, Isis also exposes a REST API (an implementation of the Restful Objects spec). All of the functionality of the domain object model is available through this REST API.

  • add Chrome extensions

  • install Postman

  • install JSON-View

  • browse to Wicket viewer, install fixtures

  • browse to the http://localhost:8080/restful API

  • invoke the service to list all objects

  • services

  • actions

  • invoke (invoking 0-arg actions is easy; the Restful Objects spec defines how to invoke N-arg actions)

3.2.15. Specify Action semantics

The semantics of an action (whether it is safe/query only, whether it is idempotent, whether it is neither) can be specified for each action; if not specified then Isis assumes non-idempotent. In the Wicket viewer this matters in that only query-only actions can be bookmarked or used as contributed properties/collections. In the RESTful viewer this matters in that it determines the HTTP verb (GET, PUT or POST) that is used to invoke the action.

  • experiment changing @Action(semantics=…​) on actions

  • note the HTTP methods exposed in the REST API change

  • note whether the non-safe actions are bookmarkable (assuming that it has been annotated with @ActionLayout(bookmarking=…​), that is).

3.2.16. Value properties

Domain entities have state: either values (primitives, strings) or references to other entities. In this section we explore adding some value properties

  • add some value properties; also:

  • for string properties

  • use the @Column(allowsNull=…​) annotation specify whether a property is optional or mandatory

  • use enums for properties (eg as used in the Isis addons example todoapp, see here and here)

  • update the corresponding domain service for creating new instances

  • for all non-optional properties will either need to prompt for a value, or calculate some suitable default

  • change the implementation of title, if need be

  • revisit the title, consider whether to use the @Title annotation

    • rather than the title() title() method

  • order the properties using the @MemberOrder, also @MemberGroupLayout

  • use the @PropertyLayout annotation to position property/action parameter labels either to the LEFT, TOP or NONE

3.2.17. Reference properties

Domain entities can also reference other domain entities. These references may be either scalar (single-valued) or vector (multi-valued). In this section we focus on scalar reference properties.

  • add some reference properties

  • update the corresponding domain service (for creation actoin)

  • use different techniques to obtain references (shown in drop-down list box)

3.2.18. Usability: Defaults

Quick detour: often we want to set up defaults to go with choices. Sensible defaults for action parameters can really improve the usability of the app.

3.2.19. Collections

Returning back to references, Isis also supports vector (multi-valued) references to another object instances� in other words collections. We sometimes called these "parented" collections (to distinguish from a "standalone" collection as returned from an action)

  • Ensure that all domain classes implement java.lang.Comparable

    • use the ObjectContracts utility class to help implement Comparable

      • you can also equals(), hashCode(), toString()

  • Add a collection to one of the entities

  • optional: use the @CollectionLayout(sortedBy=…​) annotation to specify a different comparator than the natural ordering

3.2.20. Actions and Collections

The Wicket UI doesn’t allow collections to be modified (added to/removed from). However, we can easily write actions to accomplish the same. Moreover, these actions can provide some additional business logic. For example: it probably shouldn’t be possible to add an object twice into a collection, so it should not be presented in the list of choices/autoComplete; conversely, only those objects in the collection should be offered as choices to be removed.

  • Add domain actions to add/remove from the collection

  • to create objects, inject associated domain service

    • generally we recommend using the @Inject annotation with either private or default visibility

  • the service itself should use DomainObjectContainer

  • use the @MemberOrder(name=…​) annotation to associate an action with a property or with a collection

3.2.21. CSS UI Hints

CSS classes can be associated with any class member (property, collection, action). But for actions in particular:

It’s also possible to use Font Awesome icons for the domain object icon.

So: - for some of the actions of your domain services or entities, annotate using @ActionLayout(cssClass=…​) or @ActionLayout(cssClassFa=…​)

3.2.22. Dynamic Layout

Up to this point we’ve been using annotations (@MemberOrder, @MemberGroupLayout, @Named, @PropertyLayout, @ParameterLayout, @ActionLayout and so on) for UI hints. However, the feedback loop is not good: it requires us stopping the app, editing the code, recompiling and running again. So instead, all these UI hints (and more) can be specified dynamically, using a corresponding .layout.json file. If edited while the app is running, it will be reloaded automatically (in IntelliJ, use Run>Reload Changed Classes):

  • Delete the various hint annotations and instead specify layout hints using a .layout.json file.

3.2.23. Business rules

Apache Isis excels for domains where there are complex business rules to enforce. The UI tries not to constrain the user from navigating around freely, however the domain objects nevertheless ensure that they cannot change into an invalid state. Such rules can be enforced either declaratively (using annotations) or imperatively (using code). The objects can do this in one of three ways:

  • visibility: preventing the user from even seeing a property/collection/action

  • usability: allowing the user to view a property/collection/action but not allowing the user to change it

  • validity: allowing the user to modify the property/invoke the action, but validating that the new value/action arguments are correct before hand.

Or, more pithily: "see it, use it, do it"

See it!
  • Use the Property(hidden=…​) annotation to make properties invisible

    • likewise @Collection(hidden=…​) for collections

    • the @Programmatic annotation can also be used and in many cases is to be preferred; the difference is that the latter means the member is not part of the Isis metamodel.

  • Use the hide…​() supporting method on properties, collections and actions to make a property/collection/action invisible according to some imperative rule

Use it!
Do it!

3.2.24. Home page

The Wicket UI will automatically invoke the "home page" action, if available. This is a no-arg action of one of the domain services, that can return either an object (eg representing the current user) or a standalone action.

  • Add the @HomePage annotation to one (no more) of the domain services' no-arg actions

3.2.25. Clock Service

To ensure testability, there should be no dependencies on system time, for example usage of LocalDate.now(). Instead the domain objects should delegate to the provided ClockService.

  • remove any dependencies on system time (eg defaults for date/time action parameters)

  • inject ClockService

  • call ClockService.now() etc where required.

3.2.26. Using Contributions

One of Isis' most powerful features is the ability for the UI to combine functionality from domain services into the representation of an entity. The effect is similar to traits or mix-ins in other languages, however the "mixing in" is done at runtime, within the Isis metamodel. In Isis' terminology, we say that the domain service action is contributed to the entity.

Any action of a domain service that has a domain entity type as one of its parameter types will (by default) be contributed. If the service action takes more than one argument, or does not have safe semantics, then it will be contributed as an entity action. If the service action has precisely one parameter type (that of the entity) and has safe semantics then it will be contributed either as a collection or as a property (dependent on whether it returns a collection of a scalar).

Why are contributions so useful? Because the service action will match not on the entity type, but also on any of the entity’s supertypes (all the way up to java.lang.Object). That means that you can apply the dependency inversion principle to ensure that the modules of your application have acyclic dependencies; but in the UI it can still appear as if there are bidirectional dependencies between those modules. The lack of bidirectional dependencies can help save your app degrading into a big ball of mud.

Finally, note that the layout of contributed actions/collections/properties can be specified using the .layout.json file (and it is highly recommended that you do so).

Contributed Actions
  • Write a new domain service

  • Write an action accepting >1 args:

    • one being a domain entity

    • other being a primitive or String

Contributed Collections
  • Write a new domain service (or update the one previously)

  • Write a query-only action accepting exactly 1 arg (a domain entity)

  • returning a collection, list or set

  • For this action:

  • use .layout.json to position as required

Contributed Properties
  • As for contributed collections, write a new domain service with a query-only action accepting exactly 1 arg (a domain entity); except:

    • returning a scalar value rather than a collection

  • For this action:

  • should be rendered in the UI "as if" a property of the entity

  • use .layout.json to position as required

3.2.27. Using the Event Bus

Another way in which Apache Isis helps you keep your application nicely modularized is through its event bus. Each action invocation, or property modification, can be used to generate a succession of events that allows subscribers to veto the interaction (the see it/use it/do it rules) or, if the action is allowed, to perform work prior to the execution of the action or after the execution of the action.

Under the covers Isis uses the Guava event bus and subscribers (always domain services) subscribe by writing methods annotated with @com.google.common.eventbus.Subscribe annotation.

By default the events generated are ActionDomainEvent.Default (for actions) and PropertyDomainEvent.Default (for properties). Subclasses of these can be specified using the @Action(domainEvent=…​) or Property(domainEvent=…​) for properties.

Using the guidance in the docs for the EventBusService:

  • write a domain service subscriber to subscribe to events

  • use the domain service to perform log events

  • use the domain service to veto actions (hide/disable or validate)

3.2.28. Bulk actions

Bulk actions are actions that can be invoked on a collection of actions, that is on collections returned by invoking an action. Actions are specified as being bulk actions using the @action(invokeOn=OBJECT_AND_COLLECTION) annotation.

Note that currently (1.8.0) only no-arg actions can be specified as bulk actions.

Thus: * Write a no-arg action for your domain entity, annotate with @Action(invokeOn=…​) * Inject the ActionInteractionContext (request-scoped) service * Use the ActionInteractionContext service to determine whether the action was invoked in bulk or as a regular action. * return null if invoked on a collection; the Wicket viewer will go back to the original collection ** (if return non-null, then Wicket viewer will navigate to the object of the last invocation� generally not what is required)

The similar ScratchPad (request-scoped) domain service is a good way to share information between bulk action invocations:

  • Inject the ScratchPad domain service

  • for each action, store state (eg a running total)

  • in the last invoked bulk action, perform some aggregate processing (eg calculate the average) and return

3.2.29. Performance tuning

The QueryResultsCache (request-scoped) domain service allows arbitrary objects to be cached for the duration of a request.

This can be helpful for "naive" code which would normally make the same query within a loop.

  • optional: inject the QueryResultsCache service, invoke queries "through" the cache API

  • remember that the service is request-scoped, so it only really makes sense to use this service for code that invokes queries within a loop

3.2.30. Extending the Wicket UI

Each element in the Wicket viewer (entity form, properties, collections, action button etc) is a component, each created by a internal API (ComponentFactory, described here). For collections there can be multiple views, and the Wicket viewer provides a view selector drop down (top right of each collection panel).

Moreover, we can add additional views. In this section we’ll explore some of these, already provided through Isis addons (not ASF).

Excel download

The Excel download add-on allows the collection to be downloaded as an Excel spreadsheet (.xlsx).

  • Use the instructions on the add-on module’s README to add in the excel download module (ie: update the POM).

Fullcalendar2

The Fullcalendar2 download add-on allows entities to be rendered in a full-page calendar.

  • Use the instructions on the add-on module’s README to add in the fullcalendar2 module (ie: update the POM).

  • on one of your entities, implement either the CalendarEventable interface or the (more complex) Calendarable interface.

  • update fixture scripts to populate any new properties

  • when the app is run, a collection of the entities should be shown within a calendar view

gmap3

The Gmap3 download add-on allows entities that implement certain APIs to be rendered in a full-page gmap3.

  • Use the instructions on the add-on module’s README to add in the gmap3 module (ie: update the POM).

  • on one of your entities, implement the Locatable interface

  • update fixture scripts to populate any new properties

  • when the app is run, a collection of the entities should be shown within a map view

3.2.31. Add-on modules (optional)

In addition to providing Wicket viewer extensions, Isis addons also has a large number of modules. These address such cross-cutting concerns as security, command (profiling), auditing and publishing.

3.2.32. View models

In most cases users can accomplish the business operations they need by invoking actions directly on domain entities. For some high-volume or specialized uses cases, though, there may be a requirement to bring together data or functionality that spans several entities.

Also, if using Isis' REST API then the REST client may be a native application (on a smartphone or tablet, say) that is deployed by a third party. In these cases exposing the entities directly would be inadvisable because a refactoring of the domain entity would change the REST API and probably break that REST client.

To support these use cases, Isis therefore allows you to write a view model, either by annotating the class with @ViewModel or (for more control) by implementing the ViewModel interface.

  • build a view model summarizing the state of the app (a "dashboard")

  • write a new @HomePage domain service action returning this dashboard viewmodel (and remove the @HomePage annotation from any other domain service if present)

3.2.33. Testing

Up to this point we’ve been introducing the features of Isis and building out our domain application, but with little regard to testing. Time to fix that.

Unit testing

Unit testing domain entities and domain services is easy; just use JUnit and mocking libraries to mock out interactions with domain services.

Mockito seems to be the current favourite among Java developers for mocking libraries, but if you use JMock then you’ll find we provide a JUnitRuleMockery2 class and a number of other utility classes, documented here.

  • write some unit tests (adapt from the unit tests in the myapp-dom Maven module).

Integration testing

Although unit tests are easy to write and fast to execute, integration tests are more valuable: they test interactions of the system from the outside-in, simulating the way in which the end-users use the application.

Earlier on in the tutorial we commented out the myapp-integtests module. Let’s commented it back in. In the parent pom.xml:

<modules>
    <module>dom</module>
    <module>fixture</module>
    <!--
    <module>integtests</module>
    -->
    <module>webapp</module>
</modules>

change back to:

<modules>
    <module>dom</module>
    <module>fixture</module>
    <module>integtests</module>
    <module>webapp</module>
</modules>

There will probably be some compile issues to fix up once you’ve done this; comment out all code that doesn’t compile.

Isis has great support for writing integration tests; well-written integration tests should leverage fixture scripts and use the @WrapperFactory domain service.

  • use the tests from the original archetype and the documentation on the website to develop integration tests for your app’s functionality.

3.2.34. Customising the REST API

The REST API generated by Isis conforms to the Restful Objects specification. Isis 1.8.0 provides experimental support to allow the representations to be customized.

  • as per the documentation, configure the Restful Objects viewer to generate a simplified object representation:

    isis.viewer.restfulobjects.objectPropertyValuesOnly=true

3.2.35. Configuring to use an external database

If you have an external database available, then update the pom.xml for the classpath and update the JDBC properties in WEB-INF\persistor.properties to point to your database.

4. How tos

This chapter provides instructions on how to go about actually developing Isis domain applications.

4.1. Class Structure

Apache Isis works by building a metamodel of the domain objects: entities, view models and services. The class members of both entities and view models represent both state — (single-valued) properties and (multi-valued) collections — and behaviour — actions. The class members of domain services is simpler: just behaviour, ie actions.

In the automatically generated UI a property is rendered as a field. This can be either of a value type (a string, number, date, boolean etc) or can be a reference to another entity. A collection is generally rendered as a table.

In order for Isis to build its metamodel the domain objects must follow some conventions: what we call the Apache Isis Programming Model. This is just an extension of the pojo / JavaBean standard of yesteryear: properties and collections are getters/setters, while actions are simply any remaining public methods.

Additional metamodel semantics are inferred both imperatively from supporting methods and declaratively from annotations.

In this section we discuss the mechanics of writing domain objects that comply with Isis programming model.

In fact, Isis programming model is extensible; you can teach Isis new programming conventions and you can remove existing ones; ultimately they amount to syntax. The only real fundamental that can’t be changed is the notion that objects consist of properties, collections and actions.

You can learn more about extending Isis programming model here.

4.1.1. Class Definition

WIP

Classes are defined both to Isis and (if an entity) also to JDO/DataNucleus.

We use Java packages as a way to group related domain objects together; the package name forms a namespace. We can then reason about all the classes in that package/namespace as a single unit.

In the same way that Java packages act as a namespace for domain objects, it’s good practice to map domain entities to their own (database) schemas.

For more on this topic, see the topic discussing modules and decoupling.

4.1.2. Property

TODO
Value vs Reference Types
WIP

The annotations for mapping value types tend to be different for properties vs action parameters, because JDO annotations are only valid on properties. The table in the Properties vs Parameters section provides a handy reference of each.

Optional Properties

JDO/DataNucleus' default is that a property is assumed to be mandatory if it is a primitive type (eg int, boolean), but optional if a reference type (eg String, BigDecimal etc). To override optionality in JDO/DataNucleus the @Column(allowsNull="…​") annotations is used.

Apache Isis on the other hand assumes that all properties (and action parameters, for that matter) are mandatory, not optional. These defaults can also be overridden using Isis' own annotations, specifically @Property(optionality=…​).

These different defaults can lead to incompatibilities between the two frameworks. To counteract that, Apache Isis also recognizes and honours JDO’s @Column(allowsNull=…​).

For example, rather than:

@javax.jdo.annotations.Column(allowNulls="true")
private LocalDate date;
@Property(optionality=Optionality.OPTIONAL)
public LocalDate getDate() { ... }
public void setDate(LocalDate d) { ... }

you should instead simply write:

private LocalDate date;
@javax.jdo.annotations.Column(allowNulls="true")
public LocalDate getDate() { ... }
public void setDate(LocalDate d) { ... }

With JDO/DataNucleus it’s valid for the @Column annotation to be placed on either the field or the getter. Apache Isis (currently) only looks for annotations on the getter. We therefore recommend that you always place @Column on the gettter.

In all cases the framework will search for any incompatibilities in optionality (whether specified explicitly or defaulted implicitly) between Isis' defaults and DataNucleus, and refuse to boot if any are found (fail fast).

Handling Mandatory Properties in Subtypes

If you have a hierarchy of classes then you need to decide which inheritance strategy to use.

  • "table per hierarchy", or "rollup" (InheritanceStrategy.SUPERCLASS_TABLE)

    whereby a single table corresponds to the superclass, and also holds the properties of the subtype (or subtypes) being rolled up

  • "table per class" (InheritanceStrategy.NEW_TABLE)

    whereby is a table for both superclass and subclass, in 1:1 correspondence

  • "rolldown" (InheritanceStrategy.SUBCLASS_TABLE)

    whereby a single table holds the properties of the subtype, and also holds the properties of its supertype

In the first "rollup" case, we can have a situation where - logically speaking - the property is mandatory in the subtype - but it must be mapped as nullable in the database because it is n/a for any other subtypes that are rolled up.

In this situation we must tell JDO that the column is optional, but to Isis we want to enforce it being mandatory. This can be done using the @Property(optionality=Optionality.MANDATORY) annotation.

For example:

@javax.jdo.annotations.Inheritance(strategy = InheritanceStrategy.SUPER_TABLE)
public class SomeSubtype extends SomeSuperType {
    private LocalDate date;
    @javax.jdo.annotations.Column(allowNulls="true")
    @Property(optionality=Optionality.MANDATORY)
    public LocalDate getDate() { ... }
    public void setDate(LocalDate d) { ... }
}

The @Property(optionality=…​) annotation is equivalent to the older but still supported @Optional annotation and @Mandatory annotations. Its benefit is that it lumps together all Isis' property metadata in a single annotation. Its downside is that it is rather verbose if the only semantic that needs to be specified — as is often the case — is optionality.

An alternative way to achieve this is to leave the JDO annotation on the field (where it is invisible to Isis), and rely on Isis' default, eg:

@javax.jdo.annotations.Inheritance(strategy = InheritanceStrategy.SUPER_TABLE)
public class SomeSubtype extends SomeSuperType {
    @javax.jdo.annotations.Column(allowNulls="true")
    private LocalDate date;
    // mandatory in Isis by default
    public LocalDate getDate() { }
    public void setDate(LocalDate d) { }
}

We recommend the former mapping, though, using @Property(optionality=Optionality.MANDATORY).

Strings (Length)
TODO
Mapping JODA Dates

Isis' JDO objectstore bundles DataNucleus' built-in support for Joda LocalDate and LocalDateTime datatypes, meaning that entity properties of these types will be persisted as appropriate data types in the database tables.

It is, however, necessary to annotate your properties with @javax.jdo.annotations.Persistent, otherwise the data won’t actually be persisted. See the JDO docs for more details on this.

Moreover, these datatypes are not in the default fetch group, meaning that JDO/DataNucleus will perform an additional SELECT query for each attribute. To avoid this extra query, the annotation should indicate that the property is in the default fetch group.

For example, the ToDoItem (in the todoapp example app (not ASF)) defines the dueBy property as follows:

BigDecimals (Precision)

Working with java.math.BigDecimal properties takes a little care due to scale/precision issues.

For example, suppose we have:

private BigDecimal impact;
public BigDecimal getImpact() {
    return impact;
}
public void setImpact(final BigDecimal impact) {
    this.impact = impact;
}

JDO/DataNucleus creates, at least with HSQL, the table with the field type as NUMERIC(19). No decimal digits are admitted. (Further details here).

What this implies is that, when a record is inserted, a log entry similar to this one appears:

INSERT INTO ENTITY(..., IMPACT, ....) VALUES (...., 0.5, ....)

But when that same record is retrieved, the log will show that a value of "0" is returned, instead of 0.5.

The solution is to explicitly add the scale to the field like this:

@javax.jdo.annotations.Column(scale=2)
private BigDecimal impact;
public BigDecimal getImpact() {
    return impact;
}
public void setImpact(final BigDecimal impact) {
    this.impact = impact;
}

In addition, you should also set the scale of the BigDecimal, using setScale(scale, roundingMode).

More information can be found here and here.

Mapping Blobs and Clobs

Apache Isis configures JDO/DataNucleus so that the properties of type org.apache.isis.applib.value.Blob and org.apache.isis.applib.value.Clob can also be persisted.

As for Joda dates, this requires the @javax.jdo.annotations.Persistent annotation. However, whereas for dates one would always expect this value to be retrieved eagerly, for blobs and clobs it is not so clear cut.

Mapping Blobs

For example, in the ToDoItem class (of the todoapp example app (non-ASF) the attachment property is as follows:

@javax.jdo.annotations.Persistent(defaultFetchGroup="false", columns = {
    @javax.jdo.annotations.Column(name = "attachment_name"),
    @javax.jdo.annotations.Column(name = "attachment_mimetype"),
    @javax.jdo.annotations.Column(name = "attachment_bytes", jdbcType="BLOB", sqlType = "BLOB")
})
private Blob attachment;
@Property(
        optionality = Optionality.OPTIONAL
)
public Blob getAttachment() {
    return attachment;
}
public void setAttachment(final Blob attachment) {
    this.attachment = attachment;
}

The three @javax.jdo.annotations.Column annotations are required because the mapping classes that Isis provides (IsisBlobMapping and IsisClobMapping) map to 3 columns. (It is not an error to omit these @Column annotations, but without them the names of the table columns are simply suffixed _0, _1, _2 etc.

If the Blob is mandatory, then use:

@javax.jdo.annotations.Persistent(defaultFetchGroup="false", columns = {
    @javax.jdo.annotations.Column(name = "attachment_name", allowsNull="false"),
    @javax.jdo.annotations.Column(name = "attachment_mimetype", allowsNull="false"),
    @javax.jdo.annotations.Column(name = "attachment_bytes", jdbcType="BLOB", sqlType = "BLOB",
                                  allowsNull="false")
})
private Blob attachment;
@Property(
    optionality = Optionality.MANDATORY
)
public Blob getAttachment() {
return attachment;
}
public void setAttachment(final Blob attachment) {
this.attachment = attachment;
}
Mapping Clobs

Mapping Clob`s works in a very similar way, but the `@Column#sqlType attribute will be CLOB:

@javax.jdo.annotations.Persistent(defaultFetchGroup="false", columns = {
    @javax.jdo.annotations.Column(name = "attachment_name"),
    @javax.jdo.annotations.Column(name = "attachment_mimetype"),
    @javax.jdo.annotations.Column(name = "attachment_chars", sqlType = "CLOB")
})
private Clob doc;
@Property(
    optionality = Optionality.OPTIONAL
)
public Clob getDoc() {
    return doc;
}
public void setDoc(final Clob doc) {
    this.doc = doc;
}
Mapping to VARBINARY or VARCHAR

Instead of mapping to a Blob or Clob datatype, you might also specify map to a VARBINARY or VARCHAR. In this case you will need to specify a length. For example:

@javax.jdo.annotations.Column(name = "attachment_bytes", sqlType = "VARBINARY", length=2048)

or

@javax.jdo.annotations.Column(name = "attachment_chars", sqlType = "VARCHAR", length=2048)

Support and maximum allowed length will vary by database vendor.

4.1.3. Collections

WIP

While Isis support collections of references, the framework (currently) does not support collections of values. That is, it isn’t possible to define a collection of type Set<String>.

Or, actually, you can, because that is a valid mapping supported by JDO/DataNucleus . However, Isis has no default visualization.

One workaround is to mark the collection as @Progammatic. This ensures that the collection is ignored by Isis.

Another workaround is to wrap each value in a view model, as explained in this tip.

4.1.4. Actions

TODO

While Isis support actions whose parameters' types are scalar (values such as String, int, or references such as Customer), the framework (currently) does not support parameter types that are collections or maps.

The workaround is to mark the collection as @Programmatic, as described in Ignoring Methods. This ensures that the collection is ignored by Isis.

4.1.5. Action Parameters

TODO
Strings (Length)
TODO
BigDecimals (Precision)
TODO
Optional
TODO

4.1.6. Injecting services

Apache Isis autowires (automatically injects) domain services into each entity, as well as into the domain services themselves, using either method injection or field injection. The applib DomainObjectContainer is also a service, so can be injected in exactly the same manner.

Isis currently does not support qualified injection of services; the domain service of each type must be distinct from any other.

If you find a requirement to inject two instances of type SomeService, say, then the work-around is to create trivial subclasses SomeServiceA and SomeServiceB and inject these instead.

Field Injection

Field injection is recommended, using the @javax.inject.Inject annotation. For example:

public class Customer {
    @javax.inject.Inject
    OrderRepository orderRepository;
    ...
}

We recommend using default rather than private visibility so that the field can be mocked out within unit tests (placed in the same package as the code under test).

Method Injection

Isis also supports two forms of method injection. All that is required to inject a service into a entity/service is to provide an appropriate method or field. The name of the method does not matter, only that it is prefixed either set or inject, is public, and has a single parameter of the correct type.

For example:

public class Customer {
    private OrderRepository orderRepository;
    public void setOrderRepository(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
    ...
}

or alternatively, using 'inject' as the prefix:

public class Customer {
    private OrderRepository orderRepository;
    public void injectOrderRepository(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
    ...
}

Note that the method name can be anything; it doesn’t need to be related to the type being injected.

Constructor injection

Simply to note that constructor injection is not supported by Isis (and is unlikely to be, because the JDO specification for entities requires a no-arg constructor).

4.1.7. Properties vs Parameters

WIP

In many cases the value types of properties and of action parameters align. For example, a Customer entity might have a surname property, and there might also be corresponding changeSurname. Ideally we want the surname property and surname action parameter to use the same value type.

Having first-class support for this is a goal of Isis, and it is already possible (by writing some plumbing and glue code using Isis and DataNucleus APIs) to get some way towards this ideal. However, in the vast majority of cases it is much easier and more practical to simply use standard value types such as java.lang.String.

However, the JDO/DataNucleus annotations for specifying semantics such as optionality or maximum length are restricted to only apply to fields and to methods; they cannot be applied to action parameters. It is therefore necessary to use Isis' equivalent annotations for action parameters.

The table below summarises the equivalence of some of the most common cases.

Table 1. Comparing annotations of Properties vs Action Parameters
value type/semantic property action parameter

string (length)

big decimal (precision)

Isis blob

optional

@Column(allowsNull="true")

ParameterLayout(optionality=Optionality.OPTIONAL) or @Optional

4.1.8. Ignoring Methods

WIP

Sometimes you want to define a public method on a domain object that is not intended to be rendered in Isis' UI.

To exclude such methods, use the @Programmatic annotation.

4.2. UI Hints

The Isis programming model includes several mechanisms for a domain object to provide UI hints. These range from their title (so an end-user can distinguish one object from another) through to hints that can impact their CSS styling.

4.2.1. Object Titles and Icons

In Apache Isis every object is identified to the user by a title (label) and an icon. This is shown in several places: as the main heading for an object; as a link text for an object referencing another object, and also in tables representing collections of objects.

The icon is often the same for all instances of a particular class, but it’s also possible for an individual instance to return a custom icon. This could represent the state of that object (eg a shipped order, say, or overdue library book).

It is also possible for an object to provide a CSS class hint. In conjunction with customized CSS this can be used to apply arbitrary styling; for example each object could be rendered in a page with a different background colour.

Object Title

The object title is a label to identify an object to the end-user. Generally the object title is a label to identify an object to the end-user. There is no requirement for it to be absolutely unique, but it should be "unique enough" to distinguish the object from other object’s likely to be rendered on the same page.

The title is always shown with an icon, so there is generally no need for the title to include information about the object’s type. For example the title of a customer object shouldn’t include the literal string "Customer"; it can just have the customer’s name, reference or some other meaningful business identifier.

Declarative style

The @Title annotation can be used build up the title of an object from its constituent parts.

For example:

public class Customer {
    @Title(sequence="1", append=" ")
    public String getFirstName() { ... }
    @Title(sequence="2")
    public Product getLastName() { ... }
    ...
}

might return "Arthur Clarke", while:

public class CustomerAlt {
    @Title(sequence="2", prepend=", ")
    public String getFirstName() { ... }

    @Title(sequence="1")
    public Product getLastName() { ... }
    ...
}

could return "Clarke, Arthur".

Note that the sequence is in Dewey Decimal Format. This allows a subclass to intersperse information within the title. For example (please forgive this horrible domain modelling (!)):

public class Author extends Customer {
    @Title(sequence="1.5", append=". ")
    public String getMiddleInitial() { ... }
    ...
}

could return "Arthur C. Clarke".

Titles can sometimes get be long and therefore rather cumbersome in "parented" tables. If @Title has been used then the Wicket viewer will automatically exclude portions of the title belonging to the owning object.

Imperative style
TODO - see title()
Object Icon

The icon is often the same for all instances of a particular class, but it’s also possible for an individual instance to return a custom icon. This could represent the state of that object (eg a shipped order, say, or overdue library book).

Object CSS Styling

It is also possible for an object to return a CSS class. In conjunction with customized CSS this can be used to apply arbitrary styling; for example each object could be rendered in a page with a different background colour.

4.2.2. Names and Descriptions

TODO
Action Parameters

If you’re running on Java 8, then note that it’s possible to write Isis applications without using @ParameterLayout(named=…​) annotation. Support for this can be found in the Isis addons' paraname8 metamodel extension (non-ASF). (In the future we’ll fold this into core). See also our guidance on upgrading to Java 8.

4.2.3. Layout

See the object layout chapter.

4.2.4. Eager rendering

By default, collections all rendered lazily, in other words in a "collapsed" table view:

TODO - screenshot here

For the more commonly used collections we want to show the table expanded:

TODO - screenshot here

For this we annotate the collection using the @CollectionLayout(render=RenderType.EAGERLY; for example

@javax.jdo.annotations.Persistent(table="ToDoItemDependencies")
private Set<ToDoItem> dependencies = new TreeSet<>();
@Collection
@CollectionLayout(
    render = RenderType.EAGERLY
)
public Set<ToDoItem> getDependencies() {
    return dependencies;
}

Alternatively, it can be specified the Xxx.layout.json file:

"dependencies": {
    "collectionLayout": {
        "render": "EAGERLY"
    },
}

It might be thought that collections that are eagerly rendered should also be eagerly loaded from the database by enabling the defaultFetchGroup attribute:

@javax.jdo.annotations.Persistent(table="ToDoItemDependencies", defaultFetchGroup="true")
private Set<ToDoItem> dependencies = new TreeSet<>();
...

While this can be done, it’s likely to be a bad idea, because doing so will cause DataNucleus to query for more data than required even if the object is being rendered within some referencing object’s table.

Of course, your mileage may vary, so don’t think you can’t experiment.

4.2.5. Action Icons and CSS

Apache Isis allows font awesome icons to be associated with each action, and for Bootstrap CSS to be applied to action rendered as buttons.

These UI hint can be applied either to individual actions, or can be applied en-masse using pattern matching.

Per action

Alternatively, you can specify these hints dynamically in the Xxx.layout.json for the entity.

Per pattern matching

Rather than annotating every action with @ActionLayout#cssClassFa() and @ActionLayout#cssClass() you can instead specify the UI hint globally using regular expressions.

The configuration property isis.reflector.facet.cssClassFa.patterns is a comma separated list of key:value pairs, eg:

isis.reflector.facet.cssClassFa.patterns=\
                        new.*:fa-plus,\
                        add.*:fa-plus-square,\
                        create.*:fa-plus,\
                        list.*:fa-list, \
                        all.*:fa-list, \
                        download.*:fa-download, \
                        upload.*:fa-upload, \
                        execute.*:fa-bolt, \
                        run.*:fa-bolt

where the key is a regex matching action names (eg create.*) and the value is a font-awesome icon name (eg fa-plus) to be applied (as per @CssClassFa()) to all action members matching the regex.

Similarly, the configuration property isis.reflector.facet.cssClass.patterns is a comma separated list of key:value pairs, eg:

isis.reflector.facet.cssClass.patterns=\
                        delete.*:btn-warning

where (again)the key is a regex matching action names (eg delete.*) and the value is a Bootstrap CSS button class (eg btn-warning) to be applied (as per `@CssClass()) to all action members matching the regex.

We strongly recommend that you use this technique rather than annotating each action with @ActionLayout#cssClassFa() or @ActionLayout#cssClass(). Not only is the code more maintainable, you’ll also find that it forces you to be consistent in your action names.

4.3. Domain Services

It’s worth extending the <<_hexagonal_architecture, Hexagonal Architecture> to show where domain services — and in particular the domain services provided by Isis Addons (non-ASF) — fit in:

hexagonal architecture addons
Figure 2. The hexagonal architecture with Isis addons

Here, we can see that the addons provide services both to the Isis framework (for example security, command and auditing) and also - by way of dependency injection - to the domain objects (eg tags, excel, settings). Of course, you can also write your own domain services as well, for example to interface with some external CMS system, say.

The Apache Isis framework also provides numerous in-built domain services. These are catalogued in the reference guide, see here and here.

4.3.1. Scoped services

By default all domain services are considered to be singletons, and thread-safe.

Sometimes though a service’s lifetime is applicable only to a single request; in other words it is request-scoped.

The CDI annotation @javax.enterprise.context.RequestScoped is used to indicate this fact:

@javax.enterprise.context.RequestScoped
public class MyService extends AbstractService {
    ...
}

The framework provides a number of request-scoped services, include a scratchpad service, query results caching, and support for co-ordinating bulk actions. See here and here for further details.

4.3.2. Registering domain services

TODO - discuss @DomainService if isis.services-installer=annotation-and-configuration; how to override these defaults by explicit registration

Domain services (which includes repositories and factories) can be registered in the isis.properties configuration file, under isis.services key (a comma-separated list):

For example:

isis.services = com.mycompany.myapp.employee.Employees\,
                com.mycompany.myapp.claim.Claims\,
                ...

This will then result in the framework instantiating a single instance of each of the services listed.

If all services reside under a common package, then the isis.services.prefix can specify this prefix:

isis.services.prefix = com.mycompany.myapp
isis.services = employee.Employees,\
                claim.Claims,\
                ...

This is quite rare, however; you will often want to use default implementations of domain services that are provided by the framework and so will not reside under this prefix.

Examples of framework-provided services (as defined in the applib) include clock, auditing, publishing, exception handling, view model support, snapshots/mementos, and user/application settings management; see the here and _rg_services-spi[here] for further details.

4.3.3. Contributions

WIP - just xref contributed members section.

4.3.4. Menu items

WIP - update to new annotations, including @DomainService(nature=…​)

By default every action of a service (by which we also mean repositories and factories) will be rendered in the viewer, eg as a menu item for that service menu. This behaviour can be suppressed by annotating the action using @org.apache.isis.applib.annotations.NotInServiceMenu.

For example:

public interface Library {
    @NotInServiceMenu
    public Loan borrow(Loanable l, Borrower b);
}

Note that an action annotated as being @NotInServiceMenu will still be contributed. If an action should neither be contributed nor appear in service menu items, then simply annotate it as @Hidden.

Alternatively, this can be performed using a supporting method:

public class LibraryImpl implements Library {
    public Loan borrow(Loanable l, Borrower b) { ... }
    public boolean notInServiceMenuBorrow() { ... }
}

4.3.5. Menus

WIP - update to new annotations, including @DomainService(nature=…​)

If none of the service menu items should appear, then the service itself should be annotated as @Hidden.

For example:

@Hidden
public interface EmailService {
    public void sendEmail(String to, String from, String subject, String body);
    public void forwardEmail(String to, String from, String subject, String body);
}

4.3.6. Initialization

Services can optionally declare lifecycle callbacks to initialize them (when the app is deployed) and to shut them down (when the app is undeployed).

An Isis session is available when initialization occurs (so services can interact with the object store, for example).

The framework will call any public method annotated with @PostConstruct with either no arguments of an argument of type Map<String,String>

or

In the latter case, the framework passes in the configuration (isis.properties and any other component-specific configuration files).

Shutdown is similar; the framework will call any method annotated with @PreDestroy.

4.3.7. The getId() method

Optionally, a service may provide a getId() method. This method returns a logical identifier for a service, independent of its implementation.

4.4. Object Management (CRUD)

TODO

4.4.1. Instantiating and Persisting Objects

TODO - using DomainObjectContainer's support for creation and persistence

4.4.2. Finding Objects

TODO - using DomainObjectContainer
Using DataNucleus type-safe queries
TODO - as described here

4.4.3. Deleting Objects

TODO using DomainObjectContainer's support for persistence

4.5. Entity Relationships

TODO

4.5.1. Mandatory and Optional

TODO

4.5.2. 1-m bidir relationships

When an object is added to a 1:m bidirectional relationship, the child object must refer to the parent and the child must be added to the parent’s children collection.

If there were no database involved then we would have recommended that you use the mutual registration pattern to ensure that both the parent and child are updated correctly. (The modify…​() and clear…​() supporting methods were introduced in the framework primarily to help support implement the mutual registration pattern.

However, in a relational database, these two operations in the domain object model correspond simply to updating the foreign key of the child table to reference the parent’s primary key.

So long as the parent’s children collection is a java.util.Set (rather than a Collection or a List), the JDO Objectstore will automatically maintain both sides of the relationship. All that is necessary is to set the child to refer to the parent.

For example, all you need write is:

public class Department {
    @javax.jdo.annotations.Persistent(mappedBy="department") (1)
    private SortedSet<Employee> employees = new TreeSet<Employee>();

    public SortedSet<Employee> getEmployees() { ... }
    public void setEmployees(SortedSet<Employee> employees) { ... }
    ...
}
public class Employee {
    private Department department;
    public Department getDepartment() { ... }
    public void setDepartment(Department department) { ... }
    ...
}
1 it’s the mappedBy attribute that tells DataNucleus this is a bidirectional relationship. The value "department" refers to the Employee#department property.

Moreover, when maintaining a bidirectional 1-n relationship that is automatically managed by DataNucleus, it’s preferred to "add" to the parent’s child collection, don’t set the parent on the child.

If you don’t do this then you may (as of Isis 1.4.1) hit an NullPointerException. This may be a bug in DataNucleus, we are not completely sure, but the above idiom seems to fix the issue.

For more information, see this thread on the Isis users mailing list, including this message with the above recommendation.

In fact, not only do you not need to manually maintain the relationship, we have noted on at least one occasion a subtle error if the code is programmatically added.

The error in that case was that the same object was contained in the parents collection. This of course should not happen for a TreeSet. However, JDO/DataNucleus replaces the TreeSet with its own implementation, and (either by design or otherwise) this does not enforce Set semantics.

The upshot is that you should NEVER programmatically add the child object to the parent’s collection if using JDO Objectstore.

4.6. Contributed Members

WIP - generalize to discussion on contributed collections/properties; update to new annotations

Any n-parameter action provided by a service will automatically be contributed to the list of actions for each of its (entity) parameters. From the viewpoint of the entity the action is called a contributed action.

For example, given a service:

public interface Library {
    public Loan borrow(Loanable l, Borrower b);
}

and the entities:

public class Book implements Loanable { ... }

and

public class LibraryMember implements Borrower { ... }

then the borrow(…​) action will be contributed to both Book and to LibraryMember.

This is an important capability because it helps to decouple the concrete classes from the services.

If necessary, though, this behaviour can be suppressed by annotating the service action with @org.apache.isis.applib.annotations.NotContributed.

For example:

public interface Library {
    @NotContributed
    public Loan borrow(Loanable l, Borrower b);
}

If annotated at the interface level, then the annotation will be inherited by every concrete class. Alternatively the annotation can be applied at the implementation class level and only apply to that particular implementation.

Note that an action annotated as being @NotContributed will still appear in the service menu for the service. If an action should neither be contributed nor appear in service menu items, then simply annotate it as @Hidden.

4.6.1. Contributed Action

TODO

4.6.2. Contributed Property

TODO

4.6.3. Contributed Collection

TODO

4.7. Business Rules

TODO

4.7.1. Visibility ("see it")

TODO - hide…​()
Hide a Property
Hide a Collection
Hide an Action
Hide a Contributed Property, Collection or Action
All Members Hidden

4.7.2. Usability ("use it")

Disable a Property
Disable a Collection
Disable an Action
Disable a Contributed Property, Collection or Action
All Members Unmodifiable (Disabling the Edit Button)

Sometimes an object is unmodifiable.

In the Wicket viewer this means disabling the edit button.

Declarative

@DomainObject(editing=…​)

Imperative

4.7.3. Validity ("do it")

Validate (change to) a Property
Validate (adding or removing from) a Collection
Validate (arguments to invoke) an Action
Validating a Contributed Property, Collection or Action
Declarative validation

4.8. Derived Members

TODO

4.8.1. Derived Property

TODO

4.8.2. Derived Collection

WIP

While derived properties and derived collections typically "walk the graph" to associated objects, there is nothing to prevent the returned value being the result of invoking a repository (domain service) action.

For example:

public class Customer {
    ...
    public List<Order> getMostRecentOrders() {
        return orderRepo.findMostRecentOrders(this, 5);
    }
}

4.8.3. Trigger on property change

4.8.4. Trigger on collection change

4.9. Drop Downs and Defaults

TODO

4.9.1. For Properties

WIP
Choices for Property
Auto-complete for property
Default for property

4.9.2. For Action Parameters

WIP
Choices for action parameter
Dependent choices for action params
Auto-complete for action param
Default for action param

4.9.3. For both Properties and Action Parameters

WIP
Drop-down for limited number of instances
Auto-complete (repository-based)

5. More Advanced Techniques

This chapter pulls together a number of more advanced techniques that we’ve discovered and developed while building our own Isis applications.

5.1. Decoupling

We use Java packages as a way to group related domain objects together; the package name forms a namespace. We can then reason about all the classes in that package/namespace as a single unit, or module.

This section describes how to use Isis' features to ensure that your domain application remains decoupled. The techniques described here are also the ones that have been adopted by the various Isis Addons modules (not ASF) for security, commands, auditing etc.

The following sections describe how to re-assemble an application, in particular where some modules are in-house but others are potentially third-party (eg the Isis Addons modules).

There is some overlap with OSGi and Java 9’s Jigsaw concepts of "module"; in the future we expect to refactor Isis to leverage these module systems.

5.1.1. Database Schemas

In the same way that Java packages act as a namespace for domain objects, it’s good practice to map domain entities to their own (database) schemas. As of 1.9.0-SNAPSHOT, all the Isis Addons (non-ASF) modules do this, for example:

@javax.jdo.annotations.PersistenceCapable( ...
        schema = "isissecurity",
        table = "ApplicationUser")
public class ApplicationUser ... { ... }

results in a CREATE TABLE statement of:

CREATE TABLE isissecurity."ApplicationUser" (
    ...
)

while:

@javax.jdo.annotations.PersistenceCapable( ...
        schema = "isisaudit",
        table="AuditEntry")
public class AuditEntry ... { ... }

similarly results in:

CREATE TABLE isisaudit."AuditEntry" (
    ...
)

If for some reason you don’t want to use schemas (though we strongly recommend that you do), then note that you can override the @PersistenceCapable annotation by providing XML metadata (the mappings.jdo file); see the section on configuring DataNucleus Overriding Annotations for more details.

Listener to create schema

JDO/DataNucleus does not automatically create these schema objects, but it does provide a listener callback API on the initialization of each class into the JDO metamodel.

Actually, the above statement isn’t quite true. In DN 3.2.x (as used by Isis up to v1.8.0) there was no support for schemas. As of Isis 1.9.0-SNAPSHOT and DN 4.0 there is now support. But we implemented this feature initially against DN 3.2.x, and it still works, so for now we’ve decided to leave it in.

Therefore Apache Isis attaches a listener, CreateSchemaObjectFromClassMetadata, that checks for the schema’s existence, and creates the schema if required.

The guts of its implementation is:

public class CreateSchemaObjectFromClassMetadata
        implements MetaDataListener,
                   PersistenceManagerFactoryAware,
                   DataNucleusPropertiesAware {
    @Override
    public void loaded(final AbstractClassMetaData cmd) { ... }

    protected String buildSqlToCheck(final AbstractClassMetaData cmd) {
        final String schemaName = schemaNameFor(cmd);
        return String.format(
            "SELECT count(*) FROM INFORMATION_SCHEMA.SCHEMATA where SCHEMA_NAME = '%s'", schemaName);
    }
    protected String buildSqlToExec(final AbstractClassMetaData cmd) {
        final String schemaName = schemaNameFor(cmd);
        return String.format("CREATE SCHEMA \"%s\"", schemaName);
    }
}

where MetaDataListener is the DataNucleus listener API:

public interface MetaDataListener {
    void loaded(AbstractClassMetaData cmd);
}

Although not formal API, the default CreateSchemaObjectFromClassMetadata has been designed to be easily overrideable if you need to tweak it to support other RDBMS'. Any implementation must implement org.datanucleus.metadata.MetaDataListener:

The implementation provided has has been tested for HSQLDB, PostgreSQL and MS SQL Server, and is used automatically unless an alternative implementation is specified (as described in the section below).

Alternative implementation

An alternative implementation can be registered and used through the following configuration property:

isis.persistor.datanucleus.classMetadataLoadedListener=\
        org.apache.isis.objectstore.jdo.datanucleus.CreateSchemaObjectFromClassMetadata

Because this pertains to the JDO Objectstore we suggest you put this configuration property in WEB-INF/persistor_datanucleus.properties; but putting it in isis.properties will also work.

Any implementation must implement org.datanucleus.metadata.MetaDataListener. In many cases simply subclassing from CreateSchemaObjectFromClassMetadata and overriding buildSqlToCheck(…​) and buildSqlToExec(…​) should suffice.

If you do need more control, your implementation can also optionally implement org.apache.isis.objectstore.jdo.datanucleus.PersistenceManagerFactoryAware:

public interface PersistenceManagerFactoryAware {
    public void setPersistenceManagerFactory(final PersistenceManagerFactory persistenceManagerFactory);
}

and also org.apache.isis.objectstore.jdo.datanucleus.DataNucleusPropertiesAware:

public interface DataNucleusPropertiesAware {
    public void setDataNucleusProperties(final Map<String, String> properties);
}

This latter interface provides access to the properties passed through to JDO/DataNucleus.

If you do extend Isis' CreateSchemaObjectFromClassMetadata class for some other database, please contribute back your improvements.

5.1.2. Contributions

TODO

5.1.3. Vetoing Visibility

TODO - a write-up of the "vetoing subscriber" design pattern, eg as described in the BookmarkService

eg if included an addon such as auditing or security.

solution is to write a domain event subscriber that vetoes the visibility

All the addons actions inherit from common base classes so this can be as broad-brush or fine-grained as required

5.1.5. Pushing Changes

This technique is much less powerful than using the event bus . We present it mostly for completeness.

When a property is changed

If you want to invoke functionality whenever a property is changed by the user, then you should create a supporting modifyXxx() method and include the functionality within that. The syntax is:

public void modifyPropertyName(PropertyType param) { ... }

Why not just put this functionality in the setter? Well, the setter is used by the object store to recreate the state of an already persisted object. Putting additional behaviour in the setter would cause it to be triggered incorrectly.

For example:

public class Order() {
    public Integer getAmount() { ... }
    public void setAmount(Integer amount) { ... }
    public void modifyAmount(Integer amount) { (1)
        setAmount(amount);  (3)
        addToTotal(amount); (2)
    }
    ...
}
1 The modifyAmount() method calls …​
2 …​ the addToTotal() (not shown) to maintain some running total.

We don’t want this addToCall() method to be called when pulling the object back from the object store, so we put it into the modify, not the setter.

You may optionally also specify a clearXxx() which works the same way as modify modify Xxx() but is called when the property is cleared by the user (i.e. the current value replaced by nothing). The syntax is:

public void clearPropertyName() { ... }

To extend the above example:

public class Order() {
    public Integer getAmount() { ... }
    public void setAmount(Integer amount) { ... }
    public void modifyAmount(Integer amount) { ... }
    public void clearAmount() {
        removeFromTotal(this.amount);
        setAmount(null);
    }
    ...
}
When a collection is modified

A collection may have a corresponding addToXxx() and/or removeFromXxx() method. If present, and direct manipulation of the contents of the connection has not been disabled (see ?), then they will be called (instead of adding/removing an object directly to the collection returned by the accessor).

The reason for this behaviour is to allow other behaviour to be triggered when the contents of the collection is altered. That is, it is directly equivalent to the supporting modifyXxx() and clearXxx() methods for properties (see ?).

The syntax is:

public void addTo<CollectionName>(EntityType param) { ... }

and

public void removeFromCollectionName(EntityType param) { ... }

where EntityType is the same type as the generic collection type.

For example:

public class Employee { ... }

public class Department {

    private int numMaleEmployees;                           (1)
    private int numFemaleEmployees;                         (2)

    private Set<Employee> employees = new TreeSet<Employee>();
    public Set<Employee> getEmployees() {
        return employees;
    }
    private void setEmployees(Set<Employee> employees) {
        this.employees = employees;
    }
    public void addToEmployees(Employee employee) {         (3)
        numMaleEmployees += countOneMale(employee);
        numFemaleEmployees += countOneFemale(employee);
        employees.add(employee);
    }
    public void removeFromEmployees(Employee employee) {    (4)
        numMaleEmployees -= countOneMale(employee);
        numFemaleEmployees -= countOneFemale(employee);
        employees.remove(employee);
    }
    private int countOneMale(Employee employee) { return employee.isMale()?1:0; }
    private int countOneFemale(Employee employee) { return employee.isFemale()?1:0; }

    ...
}
1 maintain a count of the number of male …​
2 …​ and female employees (getters and setters omitted)
3 the addTo…​() method increments the derived properties
4 the removeTo…​() method similarly decrements the derived properties

5.2. Overriding JDO Annotations

The JDO Objectstore (or rather, the underlying DataNucleus implementation) builds its own persistence metamodel by reading both annotations on the class and also by searching for metadata in XML files. The metadata in the XML files takes precedence over the annotations, and so can be used to override metadata that is "hard-coded" in annotations.

For example, as of 1.9.0-SNAPSHOT the various Isis addons modules (not ASF) use schemas for each entity. For example, the AuditEntry entity in the audit module is annotated as:

@javax.jdo.annotations.PersistenceCapable(
        identityType=IdentityType.DATASTORE,
        schema = "IsisAddonsAudit",
        table="AuditEntry")
public class AuditEntry {
    ...
}

This will map the AuditEntry class to a table "IsisAddonsAudit"."AuditEntry"; that is using a custom schema to own the object.

Suppose though that for whatever reason we didn’t want to use a custom schema but would rather use the default. We can override the above annotation using a package.jdo file, for example:

<?xml version="1.0" encoding="UTF-8" ?>
<jdo xmlns="http://xmlns.jcp.org/xml/ns/jdo/jdo"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/jdo/jdo
        http://xmlns.jcp.org/xml/ns/jdo/jdo_3_0.xsd" version="3.0">
    <package name="org.isisaddons.module.audit.dom">
        <class name="AuditEntry" schema="PUBLIC" table="IsisAddonsAuditEntry">
        </class>
    </package>
</jdo>

This file should be placed can be placed in src/main/java/META-INF within your application’s dom module.

  • The same approach should work for any other JDO metadata, but some experimentation might be required.+

    For example, in writing up the above example we found that writing schema="" (in an attempt to say, "use the default schema for this table") actually caused the original annotation value to be used instead.

  • Forcing the schema to "PUBLIC" (as in the above example) works, but it isn’t ideal because the name "PUBLIC" is not vendor-neutral (it works for HSQLDB, but MS SQL Server uses "dbo" as its default).

  • As of 1.9.0-SNAPSHOT Apache Isis will automatically (attempt) to create the owning schema for a given table if it does not exist. This behaviour can be customized, as described in the section on using modules.

5.3. Bulk Actions

5.4. View Models

Nature of view models

  • inmemory-entity

  • external_entity

  • application layer

Consumers of view models

  • within the Wicket viewer

  • within the REST viewer, as an external client consuming a stable API

An alternative to using view models is to map the domain object using the ContentMappingService.

5.5. Mapping RDBMS Views

TODO - as used in Estatio

5.6. Transactions and Errors

In Isis, every action invocation (in fact, every interaction) is automatically wrapped in a transaction, and any repository query automatically does a flush.

What that means is that there’s no need to explicitly start or commit transactions in 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 IsisJdoSupport domain service, then you are likely to confuse Isis and get a stack trace for your trouble.

5.6.1. Aborting Transactions

If you want to abort Isis' transaction, this can be done by throwing RecoverableException (or any subclass, eg ApplicationException). The transaction will be aborted, and the exception message will be displayed to the user.

If you throw any other exception (ie not a subclass of RecoverableException), then the users will see an error page (if Wicket viewer) or a 500 (if Restful Objects viewer).

5.6.2. Raise message/errors to users

5.6.3. Exception Recognizers

TODO - ExceptionRecognizer service

5.7. i18n

Apache Isis' support for i18n allows every element of the domain model (the class names, property names, action names, parameter names and so forth) to be translated.

It also supports translations of messages raised imperatively, by which we mean as the result of a call to title() to obtain an object’s title, or messages messages resulting from any business rule violations (eg disableXxx() or validateXxx(), and so on.

Isis does not translate the values of your domain objects, though. So, if you have a domain concept such as Country whose name is intended to be localized according to the current user, you will need to model

5.7.1. Implementation Approach

Most Java frameworks tackle i18n by using Java’s own ResourceBundle API. However, there are some serious drawbacks in this approach, such as:

  • if a string appears more than once (eg "name" or "description") then it must be translated everywhere it appears in every resource bundle file

  • there is no support for plural forms (see this SO answer)

  • there is no tooling support for translators

Apache Isis therefore takes a different approach, drawing inspiration from GNU’s gettext API and specifically its .pot and .po files. These are intended to be used as follows:

  • the .pot (portable object template) file holds the message text to be translated

  • this file is translated into multiple .po (portable object) files, one per supported locale

  • these .po files are renamed according to their locale, and placed into the 'appropriate' location to be picked up by the runtime. The name of each .po resolved in a very similar way to resource bundles.

The format of the .pot and .po files is identical; the only difference is that the .po file has translations for each of the message strings. These message strings can also have singular and plural forms.

Although Isis' implementation is modelled after GNU’s API, it does not use any GNU software. This is for two reasons: (a) to simplify the toolchain/developer experience, and (b) because GNU software is usually GPL, which would be incompatible with the Apache license).

This design tackles all the issues of ResourceBundles:

  • the .po message format is such that any given message text to translate need only be translated once, even if it appears in multiple places in the application (eg "Name")

  • the .po message format includes translations for (optional) plural form as well as singular form

  • there are lots of freely available editors to be found, many summarized on this Drupal.org webpage.

    In fact, there are also online communities/platforms of translators to assist with translating files. One such is crowdin (nb: this link does not imply endorsement).

In Isis' implementation, if the translation is missing from the .po file then the original message text from the .pot file will be returned. In fact, it isn’t even necessary for there to be any .po files; .po translations can be added piecemeal as the need arises.

5.7.2. TranslationService

The cornerstone of Isis' support for i18n is the TranslationService service. This is defined in the applib with the following API:

public interface TranslationService {
    public String translate(      (1)
            final String context,
            final String text);
    public String translate(      (2)
            final String context,
            final String singularText,
            final String pluralText,
            final int num);
    public enum Mode {
        READ,
        WRITE;
    }
    Mode getMode();               (3)
}
1 is to translate the singular form of the text
2 is to translate the plural form of the text
3 indicates whether the translation service is in read or write mode.

The translate(…​) methods are closely modelled on GNU’s gettext API. The first version is used when no translation is required, the second is when both a singular and plural form will be required, with the num parameter being used to select which is returned. In both cases the context parameter provides some contextual information for the translator; this generally corresponds to the class member.

The mode meanwhile determines the behaviour of the service. More on this below.

TranslationServicePo

Isis provides a default implementation of TranslationService, namely TranslationServicePo.

If the service is running in the normal read mode, then it simply provides translations for the locale of the current user. This means locates the appropriate .po file (based on the requesting user’s locale), finds the translation and returns it.

If however the service is configured to run in write mode, then it instead records the fact that the message was requested to be translated (a little like a spy/mock in unit testing mock), and returns the original message. The service can then be queried to discover which messages need to be translated. All requested translations are written into the .pot file.

To make the service as convenient as possible to use, the service configures itself as follows:

  • if running in prototype mode or during integration tests, then the service runs in _write mode, in which case it records all translations into the .pot file. The .pot file is written out when the system is shutdown.

  • if running in server (production) mode, then the service runs in _read mode. It is also possible to set a configuration setting in isis.properties to force read mode even if running in prototype mode (useful to manually test/demo the translations).

When running in write mode the original text is returned to the caller untranslated. If in read mode, then the translated .po files are read and translations provided as required.

5.7.3. Imperative messages

The TranslationService is used internally by Isis when building up the metamodel; the name and description of every class, property, collection, action and action parameter is automatically translated. Thus the simple act of bootstrapping Isis will cause most of the messages requiring translation (that is: those for the Isis metamodel) to be captured by the TranslationService.

However, for an application to be fully internationalized, any validation messages (from either disableXxx() or validateXxx() supporting methods) and also possibly an object’s title (from the title() method) will also require translation. Moreover, these messages must be captured in the .pot file such that they can be translated.

TranslatableString

The first part of the puzzle is tackled by an extension to Isis' programming model. Whereas previously the disableXxx() / validateXxx() / title() methods could only return a java.lang.String, they may now optionally return a TranslatableString (defined in Isis applib) instead.

Here’s a (silly) example from the SimpleApp archetype:

public TranslatableString validateUpdateName(final String name) {
    return name.contains("!")? TranslatableString.tr("Exclamation mark is not allowed"): null;
}

This corresponds to the following entry in the .pot file:

#: dom.simple.SimpleObject#updateName()
msgid "Exclamation mark is not allowed"
msgstr ""

The full API of TranslatableString is modelled on the design of GNU gettext (in particular the gettext-commons library):

public final class TranslatableString {
    public static TranslatableString tr(       (1)
            final String pattern,
            final Object... paramArgs) { ... }
    public static TranslatableString trn(      (2)
            final String singularPattern,
            final String pluralPattern,
            final int number,
            final Object... paramArgs) { ... }
    public String translate(                   (3)
            final TranslationService translationService,
            final String context) { ... }
}
1 returns a translatable string with a single pattern for both singular and plural forms.
2 returns a translatable string with different patterns for singular and plural forms; the one to use is determined by the 'number' argument
3 translates the string using the provided TranslationService, using the appropriate singular/regular or plural form, and interpolating any arguments.

The interpolation uses the format {xxx}, where the placeholder can occur multiple times.

For example:

final TranslatableString ts = TranslatableString.tr(
    "My name is {lastName}, {firstName} {lastName}.",
    "lastName", "Bond", "firstName", "James");

would interpolate (for the English locale) as "My name is Bond, James Bond".

For a German user, on the other hand, if the translation in the corresponding .po file was:

#: xxx.yyy.Whatever#context()
msgid "My name is {lastName}, {firstName} {lastName}."
msgstr "Iche heisse {firstName} {lastName}."

then the translation would be: "Ich heisse James Bond".

The same class is used in DomainObjectContainer so that you can raise translatable info, warning and error messages; each of the relevant methods are overloaded.

For example:

public interface DomainObjectContainer {
    void informUser(String message);
    void informUser(
        TranslatableMessage message,
        final Class<?> contextClass, final String contextMethod); (1)
    ...
}
1 are concatenated together to form the context for the .pot file.
TranslatableException

Another mechanism by which messages can be rendered to the user are as the result of exception messages thrown and recognized by an ExceptionRecognizer.

In this case, if the exception implements TranslatableException, then the message will automatically be translated before being rendered. The TranslatableException itself takes the form:

public interface TranslatableException {
    TranslatableString getTranslatableMessage(); (1)
    String getTranslationContext();              (2)
}
1 the message to translate. If returns null, then the Exception#getMessage() is used as a fallback
2 the context to use when translating the message

5.7.4. Integration Testing

So much for the API; but as noted, it is also necessary to ensure that the required translations are recorded (by the TranslationService) into the .pot file.

For this, we recommend that you ensure that all such methods are tested through an integration test (not unit test).

For example, here’s the corresponding integration test for the "Exclamation mark" example from the simpleapp (above):

@Rule
public ExpectedException expectedException = ExpectedException.none();

@Inject
FixtureScripts fixtureScripts;

@Test
public void failsValidation() throws Exception {
    // given
    RecreateSimpleObjects fs = new RecreateSimpleObjects().setNumber(1);
    fixtureScripts.runFixtureScript(fs, null);
    SimpleObject simpleObjectWrapped = wrap(fs.getSimpleObjects().get(0));

    // expect
    expectedExceptions.expect(InvalidException.class);
    expectedExceptions.expectMessage("Exclamation mark is not allowed");

    // when
    simpleObjectWrapped.updateName("new name!");
}

Running this test will result in the framework calling the validateUpdateName(…​) method, and thus to record that a translation is required in the .pot file.

When the integration tests are complete (that is, when Isis is shutdown), the TranslationServicePo will write out all captured translations to its log (more on this below). This will include all the translations captured from the Isis metamodel, along with all translations as exercised by the integration tests.

To ensure your app is fully internationalized app, you must therefore:

  • use TranslatableString rather than String for all validation/disable and title methods.

  • ensure that (at a minimum) all validation messages and title methods are integration tested.

We make no apologies for this requirement: one of the reasons that we decided to implement Isis' i18n support in this way is because it encourages/requires the app to be properly tested.

Behind the scenes Isis uses a JUnit rule (ExceptionRecognizerTranslate) to intercept any exceptions that are thrown. These are simply passed through to the registered ExceptionRecognizers so that any messages are recorded as requiring translation.

5.7.5. Configuration

There are several different aspects of the translation service that can be configured.

Logging

To configure the TranslationServicePo to write to out the translations.pot file, add the following to the integtests logging.properties file:

log4j.appender.translations-po=org.apache.log4j.FileAppender
log4j.appender.translations-po.File=./translations.pot
log4j.appender.translations-po.Append=false
log4j.appender.translations-po.layout=org.apache.log4j.PatternLayout
log4j.appender.translations-po.layout.ConversionPattern=%m%n

log4j.logger.org.apache.isis.core.runtime.services.i18n.po.PoWriter=INFO,translations-po
log4j.additivity.org.apache.isis.core.runtime.services.i18n.po.PotWriter=false

Just to repeat, this is not the WEB-INF/logging.properties file, it should instead be added to the integtests/logging.properties file.

Location of the .po files

The default location of the translated .po files is in the WEB-INF directory. These are named and searched for similarly to regular Java resource bundles.

For example, assuming these translations:

/WEB-INF/translations-en-US.po
        /translations-en.po
        /translations-fr-FR.po
        /translations.po

then:

  • a user with en-US locale will use translations-en-US.po

  • a user with en-GB locale will use translations-en.po

  • a user with fr-FR locale will use translations-fr-FR.po

  • a user with fr-CA locale will use translations.po

The basename for translation files is always translations; this cannot be altered.

Externalized translation files

Normally Isis configuration files are read from the WEB-INF file. However, Isis can be configured to read config files from an external directory; this is also supported for translations.

Thus, if in web.xml the external configuration directory has been set:

<context-param>
    <param-name>isis.config.dir</param-name>
    <param-value>location of external config directory</param-value>
</context-param>

Then this directory will be used as the base for searching for translations (rather than the default 'WEB-INF/' directory).

Force read mode

As noted above, if running in prototype mode then TranslationServicePo will be in write mode, if in production mode then will be in read mode. To force read mode (ie use translations) even if in prototype mode, add the following configuration property to isis.properties:

isis.services.translation.po.mode=read

5.7.6. Supporting services

The TranslationServicePo has a number of supporting/related services.

LocaleProvider

The LocaleProvider API is used by the TranslationServicePo implementation to obtain the locale of the "current user".

A default implementation is provided by the Wicket viewer.

Note that this default implementation does not support requests made through the Restful Objects viewer (there is no Wicket 'application' object available); the upshot is that requests through Restful Objects are never translated. Registering a different implementation of LocaleProvider that taps into appropriate REST (RestEasy?) APIs would be the way to address this.

TranslationsResolver

The TranslationResolver is used by the TranslationService implementation to lookup translations for a specified locale. It is this service that reads from the WEB-INF/ (or externalized directory).

TranslationServicePoMenu

The TranslationServicePoMenu provides a couple of menu actions in the UI (prototype mode only) that interacts with the underlying TranslationServicePo:

  • the downloadTranslationsFile() action - available only in write mode - allows the current .pot file to be downloaded.

    While this will contain all the translations from the metamodel, it will not necessarily contain all translations for all imperative methods returning TranslatableString instances; which are present and which are missing will depend on which imperative methods have been called (recorded by the service) prior to downloading.

  • the clearTranslationsCache() action - available only in read mode - will clear the cache so that new translations can be loaded.

    This allows a translator to edit the appropriate translations-xx-XX.po file and check the translation is correct without having to restart the app.

5.8. Multi-tenancy

TODO - as supported by (non-ASF) Isis addons' security module.

5.9. Persistence Lifecycle

TODO

5.10. Tips n Tricks

This section catalogues a miscellaneous collection of hints-and-tips.

5.10.1. 'Are you sure?' idiom

If providing an action that will perform irreversible changes, include a mandatory boolean parameter that must be explicitly checked by the end-user in order for the action to proceed.

Screenshots

For example:

are you sure

Note that these screenshots shows an earlier version of the Wicket viewer UI (specifically, pre 1.8.0).

If the user checks the box:

are you sure happy case

then the action will complete.

However, if the user fails to check the box, then a validation message is shown:

are you sure sad case
Code example

The code for this is pretty simple:

public List<ToDoItem> delete(@Named("Are you sure?") boolean areYouSure) {
    container.removeIfNotAlready(this);
    container.informUser("Deleted " + container.titleOf(this));
    return toDoItems.notYetComplete();          (1)
}
public String validateDelete(boolean areYouSure) {
    return areYouSure? null: "Please confirm you are sure";
}
1 invalid to return this (cannot render a deleted object)

Note that the action itself does not use the boolean parameter, it is only used by the supporting validate…​() method.

5.10.2. Collections of values

Although in Apache Isis you can have properties of either values (string, number, date etc) or of (references to other) entities, with collections the framework (currently) only supports collections of (references to) entities. That is, collections of values (a bag of numbers, say) are not supported.

However, it is possible to simulate a bag of numbers using view models.

View Model
TODO
Persistence Concerns
WIP - easiest to simply store using DataNucleus' support for collections, marked as @Programmatic so that it is ignored by Isis. Alternatively can store as json/xml in a varchar(4000) or clob and manually unpack.

5.10.3. 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 getEffectveValue() { ... }
    ...
}

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

For more background on this workaround, see ISIS-582.

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;        // workaround for ISIS-582
    }
    ...
}

Alternatively the implementation could throw a RuntimeException, eg

throw new RuntimeException("never called; workaround for ISIS-582");

5.10.4. Per-user Themes

From the Isis mailing list is:

  • Is it possible to have each of our resellers (using our Isis application) use there own theme/branding with their own logo and colors? Would this also be possible for the login page, possibly depending on the used host name?

Yes, you can do this, by installing a custom implementation of the Wicket Bootstrap’s ActiveThemeProvider.

The Isis addons' todoapp (non-ASF) actually does this, storing the info via the Isis addons' settings module settings modules:

IActiveThemeProvider implementation
public class UserSettingsThemeProvider implements ActiveThemeProvider {
    ...
    @Override
    public ITheme getActiveTheme() {
        if(IsisContext.getSpecificationLoader().isInitialized()) {
            final String themeName = IsisContext.doInSession(new Callable<String>() {
                @Override
                public String call() throws Exception {
                    final Class<UserSettingsService> serviceClass = UserSettingsService.class;
                    final UserSettingsService userSettingsService = lookupService(serviceClass);
                    final UserSetting activeTheme = userSettingsService.find(
                            IsisContext.getAuthenticationSession().getUserName(), ACTIVE_THEME);
                    return activeTheme != null ? activeTheme.valueAsString() : null;
                }
            });
            return themeFor(themeName);
        }
        return new SessionThemeProvider().getActiveTheme();
    }
    @Override
    public void setActiveTheme(final String themeName) {
        IsisContext.doInSession(new Runnable() {
            @Override
            public void run() {
                final String currentUsrName = IsisContext.getAuthenticationSession().getUserName();
                final UserSettingsServiceRW userSettingsService =
                        lookupService(UserSettingsServiceRW.class);
                final UserSettingJdo activeTheme =
                        (UserSettingJdo) userSettingsService.find(currentUsrName, ACTIVE_THEME);
                if(activeTheme != null) {
                    activeTheme.updateAsString(themeName);
                } else {
                    userSettingsService.newString(
                        currentUsrName, ACTIVE_THEME, "Active Bootstrap theme for user", themeName);
                }
            }
        });
    }
    private ITheme themeFor(final String themeName) {
        final ThemeProvider themeProvider = settings.getThemeProvider();
        if(themeName != null) {
            for (final ITheme theme : themeProvider.available()) {
                if (themeName.equals(theme.name()))
                    return theme;
            }
        }
        return themeProvider.defaultTheme();
    }
    ...
}

and

Using the ActiveThemeProvider
@Override
protected void init() {
    super.init();

    final IBootstrapSettings settings = Bootstrap.getSettings();
    settings.setThemeProvider(new BootswatchThemeProvider(BootswatchTheme.Flatly));

    settings.setActiveThemeProvider(new UserSettingsThemeProvider(settings));
}

5.10.5. Enhance only (IntelliJ)

From the Isis mailing list is:

  • Is there a simple way to make a run configuration in IntelliJ for running the datanucleus enhancer before running integration test?

Yes, you can; here’s one way:

  • Duplicate your run configuration for running the webapp

    • the one where the main class is org.apache.isis.WebServer

    • there’s a button for this on the run configurations dialog.

  • then, on your copy change the main class to org.apache.isis.Dummy

6. Wicket Viewer

This chapter describes end-user features, configuration and customization of the Wicket viewer.

Note that extending the Wicket viewer is described in the Extending chapter.

6.1. Features/end-user usage

This section discusses features of the wicket viewer from the perspective of an end-user actually using your Isis application.

6.1.1. Recent pages (drop down)

The Wicket viewer provides a recent pages drop-down that acts as a breadcrumb trail. Using it, the user can quickly open a recently accessed domain object.

Screenshots

The following screenshot, taken from the Estatio application, shows the recent pages drop-down after a number of pages have been accessed.

recent pages

Note that this screenshot show an earlier version of the Wicket viewer UI (specifically, pre 1.8.0).

Domain Code

The recent pages drop-down is automatically populated; no changes need to be made to the domain classes.

User Experience

Selecting the domain object from the list causes the viewer to automatically navigate to the page for the selected object.

The bookmarked pages (sliding panel) also provides links to recently visited objects, but only those explicitly marked as @DomainObject(bookmarking=…​). The bookmarks panel also nests related objects together hierarchically (the recent pages drop-down does not).

Configuration

The number of objects is hard-coded as 10; it cannot currently be configured.

6.1.2. Bookmarked pages

The Wicket viewer supports the bookmarking of both domain objects and query-only (@Action(semantics=…​)) actions.

Domain objects, if bookmarkable, can be nested.

Bookmarking is automatic; whenever a bookmarkable object/action is visited, then a bookmark is created. To avoid the number of bookmarks from indefinitely growing, bookmarks that have not been followed after a whle are automatically removed (an MRU/LRU algorithm). The number of bookmarks to preserve can be configured.

Screenshots

The following screenshot, taken from Isisaddons example todoapp (not ASF) shows how the bookmarks are listed in a sliding panel.

panel

Note that these screenshots show an earlier version of the Wicket viewer UI (specifically, pre 1.8.0).

Note how the list contains both domain objects and an action ("not yet complete").

Bookmarks can also form a hierarchy. The following screenshot, also taken from the Estatio application, shows a variety of different bookmarked objects with a nested structure:

panel estatio

Some - like Property, Lease and Party - are root nodes. However, LeaseItem is bookmarkable as a child of Lease, and LeaseTerm is bookmarkable only as a child of LeaseItem. This parent/child relationship is reflected in the layout.

Domain Code

To indicate a class is bookmarkable, use the @DomainObjectLayout annotation:

@DomainObjectLayout(
    bookmarking=BookmarkPolicy.AS_ROOT
)
public class Lease { ... }

To indicate a class is bookmarkable but only as a child of some parent bookmark, specify the bookmark policy:

@DomainObjectLayout(
    bookmarking=BookmarkPolicy.AS_CHILD
)
public class LeaseItem { ... }

To indicate that a safe (query only) action is bookmarkable, use the @ActionLayout annotation:

public class ToDoItem ... {
    ...
    @ActionLayout(
         bookmarking=BookmarkPolicy.AS_ROOT
     )
    @ActionSemantics(Of.SAFE)
    public List<ToDoItem> notYetComplete() { ... }
    ...
}

The BookmarkPolicy.AS_CHILD does not have a meaning for actions; if the bookmarking attribute is set to any other value, it will be ignored.

User Experience

The sliding panel appears whenever the mouse pointer hovers over the thin blue tab (to the left of the top header region).

Alternatively, alt+[ will toggle open/close the panel; it can also be closed using Esc key.

The Recent Pages also lists recently visited pages, selected from a drop-down.

Configuration

By default, the bookmarked pages panel will show a maximum of 15 'root' pages. This can be overridden using a property (in isis.properties), for example:

isis.viewer.wicket.bookmarkedPages.maxSize=20

6.1.3. Hints and copy URL

While the user can often copy the URL of a domain object directly from the browser’s address bar, the Wicket viewer also allows the URL of domain objects to be easily copied from a dialog.

More interestingly, this URL can also contain hints capturing any sorting or page numbering, or hiding/viewing of collections. End-users can therefore share these URLs as a form of deep linking into a particular view on a domain object.

The copy URL and hinting is automatic.

Screenshots

The following screenshots are taken from the [Estatio](https://github.com/estatio/estatio) application.

Copy URL

This screenshot shows the copy URL button (top right):

010 copy link button

Note that these screenshots show an earlier version of the Wicket viewer UI (specifically, pre 1.8.0).

Clicking on this button brings up a dialog with the URL preselected:

020 copy link dialog

The URL in this case is something like:

http://localhost:8080/wicket/entity/org.estatio.dom.lease.Lease:0

The user can copy the link (eg ctrl+C) into the clipboard, then hit OK or Esc to dismiss the dialog.

Hints

Using the viewer the user can hide/show collection tables, can sort the tables by header columns:

030 hints

Also, if the collection spans multiple pages, then the individual page can be selected.

Once the view has been customised, the URL shown in the copy URL dialog is in an extended form:

040 copy link with hints

The URL in this case is something like:

http://localhost:8080/wicket/entity/org.estatio.dom.lease.Lease:0?hint-1:collectionContents-view=3&hint-1:collectionContents:collectionContents-3:table-DESCENDING=value&hint-1:collectionContents:collectionContents-3:table-pageNumber=0&hint-2:collectionContents-view=0&hint-2:collectionContents:collectionContents-2:table-pageNumber=0&hint-3:collectionContents-view=2&hint-3:collectionContents:collectionContents-2:table-pageNumber=0&hint-4:collectionContents-view=3&hint-4:collectionContents:collectionContents-3:table-ASCENDING=exerciseDate&hint-4:collectionContents:collectionContents-3:table-pageNumber=0&hint-5:collectionContents-view=0&hint-5:collectionContents:collectionContents-3:table-pageNumber=0
Copy URL from title

When the user invokes an action on the object, the URL (necessarily) changes to indicate that the action was invoked. This URL is specific to the user’s session and cannot be shared with others.

A quick way for the user to grab a shareable URL is simply by clicking on the object’s title:

050 title url
User Experience

The copy URL dialog is typically obtained by clicking on the icon.

Alternatively, alt+] will also open the dialog. It can be closed with either OK or the Esc key.

6.1.4. Titles in Tables

Object titles can often be quite long if the intention is to uniquely identify the object. While this is appropriate for the object view, it can be cumbersome within tables.

If an object’s title is specified with from @Title annotation then the Wicket viewer will (for parented collections) automatically "contextualize" a title by excluding the part of the title corresponding to a reference to the owning (parent) object.

In other words, suppose we have:

cust order product

so that Customer has a collection of `Order`s:

public class Customer {
    public Set<Order> getOrders() { ... }
    ...
}

and Product also has a collection of `Order`s (please forgive the suspect domain modelling in this example (!)):

public class Product {
    public Set<Order> getOrders() { ... }
    ...
}

and where the Order class references both Customer and Product.

The `Order’s might involve each of these:

public class Order {
    @Title(sequence="1")
    public Customer getCustomer() { ... }
    @Title(sequence="2")
    public Product getProduct() { ... }
    @Title(sequence="3")
    public String getOtherInfo() { ... }
    ...
}

In this case, if we view a Customer with its collection of Order`s, then in that parented collection’s table the customer’s property will be automatically excluded from the title of the `Order (but it would show the product). Conversely, if a Product is viewed then its collection of `Order`s would suppress product (but would show the customer).

This feature is a close cousin of the @PropertyLayout(hidden=Where.REFERENCES_PARENT) annotation, which will cause the property itself to be hidden as a column in the table. An Isis idiom is therefore:

public class Order {
    @Title(sequence="1")
    @PropertyLayout(hidden=Where.REFERENCES_PARENT)
    public Customer getCustomer() { ... }
    ...
}

The above annotations mean that titles usually "just work", altering according to the context in which they are viewed.

It is also possible to configure the Wicket viewer to abbreviate titles or suppress them completely.

6.1.5. File upload/download

The Isis application library provides the Blob value type (binary large objects) and also the Clob value type (character large object), each of which also includes metadata about the data (specifically the filename and mime type).

A class can define a property using either of these types, for example:

Screenshots

The following screenshots are taken from the Isis addons example todoapp (not ASF):

View mode, empty

Blob field rendered as attachment (with no data):

010 attachment field 940

Note that these screenshots show an earlier version of the Wicket viewer UI (specifically, pre 1.8.0).

Edit mode

Hit edit; 'choose file' button appears:

020 edit choose file 940
Choose file

Choose file using the regular browser window:

030 choose file using browser 520

Chosen file is indicated:

040 edit chosen file indicated 940
Image rendered

Back in view mode (ie once hit OK) if the Blob is an image, then it is shown:

050 ok if image then rendered 940
Download

Blob can be downloaded:

060 download 940
Clear

Back in edit mode, can choose a different file or clear (assuming property is not mandatory):

070 edit clear 940
Domain Code

To define a Blob, use:

private Blob attachment;
@javax.jdo.annotations.Persistent(defaultFetchGroup="false")
    @javax.jdo.annotations.Persistent(defaultFetchGroup="false", columns = {
            @javax.jdo.annotations.Column(name = "attachment_name"),
            @javax.jdo.annotations.Column(name = "attachment_mimetype"),
            @javax.jdo.annotations.Column(name = "attachment_bytes", jdbcType = "BLOB", sqlType = "BLOB")
    })
@Property(
        domainEvent = AttachmentDomainEvent.class,
        optionality = Optionality.OPTIONAL
)
public Blob getAttachment() { return attachment; }
public void setAttachment(final Blob attachment) { this.attachment = attachment; }

To define a Clob, use:

private Clob doc;
@javax.jdo.annotations.Persistent(defaultFetchGroup="false", columns = {
        @javax.jdo.annotations.Column(name = "doc_name"),
        @javax.jdo.annotations.Column(name = "doc_mimetype"),
        @javax.jdo.annotations.Column(name = "doc_chars", jdbcType = "CLOB", sqlType = "CLOB")
})
@Property(
        optionality = Optionality.OPTIONAL
)
public Clob getDoc() { return doc; }
public void setDoc(final Clob doc) { this.doc = doc; }

The Blob and Clob types can also be used as parameters to actions.

6.1.6. User Registration

The Wicket viewer provides the ability for users to sign-up by providing a valid email address:

  • from the login page the user can instead follow a link to take them to a sign-up page, where they enter their email address.

  • a verification email is sent using this service; the email includes a link back to the running application.

  • the user then completes the registration process by choosing a user name and password.

  • the Wicket viewer then creates an account for them and logs them in.

In a similar way, if the user has forgotten their password then they can request a reset link to be sent to their email, again by providing their email address.

To support this the framework requires three services to be registered and configured:

The Isis core framework provides a default implementation of both the email notification service and the email service. If your application uses the Isis addons security module (not ASF) then an implementation is provided by that module; just add to the classpath. Otherwise you will need to provide your own implementation.

There is no default implementation of the user registration service in the core framework.

Screenshots

The user is presented with a login page:

login page default

Navigate to the sign up page. Complete the page, and verify:

sign up page

Back to the login page:

sign up login page after sign up

Email arrives, with link:

sign up email with verification link

Follow the link, complete the page:

sign up registration page

Automatically logged in:

sign up after registration
Configuration

There are two prerequisites:

The latter is required if you are using the default email notification service and email service. If you are using your own alternative implementation of the email notification service then it may be omitted (and configure your own alternative implementation as required).

It is also possible to configure the Wicket viewer to suppress the sign-up page link and/or the password reset page.

6.2. Configuration Properties

Wicket configuration properties alter the way in which Isis' Wicket viewer renders domain objects. They are typically stored in WEB-INF/viewer_wicket.properties.

To tell Isis that the Wicket viewer is in use (and should therefore search for the viewer_wicket.properties file), add the following to WEB-INF/web.xml:

<context-param>
    <param-name>isis.viewers</param-name>
    <param-value>wicket</param-value>
</context-param>

If you prefer you can place all configuration properties into WEB-INF/isis.properties (the configuration properties from all config files are merged together).

Table 2. Wicket Viewer Configuration Properties
Property Value
(default value)
Description

isis.viewer.wicket.
bookmarkedPages

+ve int (15)

number of pages to bookmark

isis.viewer.wicket.
disableDependentChoiceAutoSelection

true,false (false)

For dependent choices, whether to automatically select the first dependent (eg subcategory) when the parameter on which it depends (category) changes.

isis.viewer.wicket.
disableModalDialogs

true,false (false)

By default the Isis Wicket viewer uses a modal dialog for action parameters. Before this feature was implemented (prior to 1.4.0), Isis rendered action parameters on its own page. This property re-enables the old behaviour.

Note that action pages are still used for bookmarked actions.

isis.viewer.wicket.
maxTitleLengthInParentedTables

+ve integer, (12)

See discussion below.

isis.viewer.wicket.
maxTitleLengthInStandaloneTables

+ve integer, (12)

See discussion below.

isis.viewer.wicket.
maxTitleLengthInTables

+ve integer, (12)

See discussion below.

isis.viewer.wicket.
regularCase

true,false (false)

Ignored for 1.8.0+; in earlier versions forced regular case rather than title case in the UI

isis.viewer.wicket.
stripWicketTags

true,false (true)

Whether to force Wicket tags to be stripped in prototype/development mode. See discussion below.

isis.viewer.wicket.
suppressPasswordReset

true,false (false)

If user registration is enabled, whether to suppress the "password reset" link on the login page. See discussion below.

isis.viewer.wicket.
suppressRememberMe

true,false (false)

Whether to suppress "remember me" checkbox on the login page.

isis.viewer.wicket.
suppressSignUp

true,false (false)

If user registration is enabled, whether to suppress the "sign up" link on the login page. See discussion below.

isis.viewer.wicket.
themes.enabled

comma separated list …​

…​ of bootswatch themes. Only applies if themes.showChooser==true. See discussion below.

isis.viewer.wicket.
themes.showChooser

true,false (false)

Whether to show chooser for Bootstrap themes. See discussion below

6.2.1. Abbreviating/suppressing titles in tables

Objects whose title is overly long can be cumbersome in titles. The Wicket viewer has a mechanism to automatically shorten the titles of objects specified using @Title. As an alternative/in addition, the viewer can also be configured to simply truncate titles longer than a certain length.

The properties themselves are:

isis.viewer.wicket.maxTitleLengthInStandaloneTables=20
isis.viewer.wicket.maxTitleLengthInParentedTables=8

If you wish to use the same value in both cases, you can also specify just:

isis.viewer.wicket.maxTitleLengthInTables=15

This is used as a fallback if the more specific properties are not provided.

If no properties are provided, then the Wicket viewer defaults to abbreviating titles to a length of 12.

6.2.2. Suppressing 'remember me'

The 'remember me' checkbox on the login page can be suppressed, if required, by setting a configuration flag.

Screenshots

With 'remember me' not suppressed (the default):

login page default

and with the checkbox suppressed:

login page suppress remember me
Configuration

To suppress the 'remember me' checkbox, add the following configuration flag:

isis.viewer.wicket.suppressRememberMe=true

6.2.3. Suppressing 'sign up'

If user registration has been configured, then the Wicket viewer allows the user to sign-up a new account and to reset their password from the login page.

The 'sign up' link can be suppressed, if required, by setting a configuration flag.

Screenshots

With 'sign up' not suppressed (the default):

login page default

and with the link suppressed:

login page suppress sign up
Configuration

To suppress the 'sign up' link, add the following configuration flag:

isis.viewer.wicket.suppressSignUp=true
See also

The password reset link can be suppressed in a similar manner.

6.2.4. Suppressing 'password reset'

If user registration has been configured, then the Wicket viewer allows the user to sign-up a new account and to reset their password from the login page.

The 'password reset' link can be suppressed, if required, by setting a configuration flag.

Screenshots

With 'password reset' not suppressed (the default):

login page default

and with the link suppressed:

login page suppress password reset
Configuration

To suppress the 'password reset' link, add the following configuration flag:

isis.viewer.wicket.suppressPasswordReset=true

Typically this should be added to the viewer_wicket.properties file (in WEB-INF), though you can add to isis.properties if you wish.

See also

The sign up link can be suppressed in a similar manner.

6.2.5. Stripped Wicket tags

By default the Isis Wicket viewer will always strip wicket tags. However, when running in prototype mode, this behaviour can be overridden using a configuration property:

isis.viewer.wicket.stripWicketTags=false

In 1.7.0 and earlier, the behaviour is different; the Isis Wicket viewer will preserve wicket tags when running in Isis' prototype/development mode, but will still strip wicket tags in Isis' server/deployment mode.

We changed the behaviour in 1.8.0 because we found that Internet Explorer can be sensitive to the presence of Wicket tags.

6.2.6. Showing a theme chooser

The Wicket viewer uses Bootstrap styles and components (courtesy of the Wicket Bootstrap integration).

Unless a default theme has been specified, the viewer uses the default bootstrap theme. However, the viewer can also be configured to allow the end-user to switch theme to another theme, in particular one of those provided by bootswatch.com.

This is done using the following configuration property (in WEB-INF/viewer_wicket.properties):

isis.viewer.wicket.themes.showChooser=true
example 1
Figure 3. Example 1
example 2
Figure 4. Example 2:

It is also possible to restrict the themes shown to some subset of those in bootswatch. This is done using a further property:

isis.viewer.wicket.themes.enabled=bootstrap-theme,Cosmo,Flatly,Darkly,Sandstone,United

where the value is the list of themes (from bootswatch.com) to be made available.

You can also develop and install a custom themes (eg to fit your company’s look-n-feel/interface guidelines); see the Extending chapter for further details.

6.3. Request Parameters

This section describes features that are dependent on HTTP request parameters (not by setting configuration properties in isis.properties).

The Wicket viewer provides some support such that an Isis application can be embedded within a host webapp, for example within an iframe.

Currently this support consists simply of being able to suppress the header and/or footer.

Screenshots

For example, the regular view is:

regular

With the header and footer both suppressed only the main content is shown:

no header no footer

It is also possible to suppress just the header, or just the footer.

Request parameters

To suppress the header, add the following as a request parameter:

isis.no.header

and to suppress the header, add the following as a request parameter:

isis.no.footer

For example,

http://localhost:8080/wicket/entity/TODO:0?isis.no.header&isis.no.footer

6.4. Layout

The wicket viewer has full support for the various methods of specifying the layout of objects, either statically or dynamically.

Dynamic reloading is automatically supported when running in prototype (development) mode, the Wicket viewer automatically rebuilds the metamodel for each class as it is rendered. Conversely, do note that the Wicket viewer will _not pick up new metadata if running in production/server mode.

To pick up a change, edit the .layout.json file in the IDE and rebuild. Then click on the title of the object *twice*.

The first click will cause Isis to pick up the new metadata, but too late in the process to adjust the rendering. The second click will re-render the object with the new metadata.

The screenshots below show some of the effects that can be accomplished, for the open source Estatio app.

First, an Invoice. This places the properties in three equal-sized columns, with collections underneath:

estatio Invoice

Next, Lease. This places its properties in two columns (one wide, one narrow), with its collections shown in the right-most column:

estatio Lease

Next, LeaseItem. This is similar to Lease, with two columns for properties and the third column for collections:

estatio LeaseItem

And finally, from the (non-ASF) Isis addons' todoapp, its ToDoItem. This puts its properties in two columns; the collections (not visible in the screenshot below) are underneath (like Estatio’s Invoice):

todoapp ToDoItem

To learn more about how to configure these types of layout, see the object layout chapter.

6.4.1. Screencast

How to layout properties and collections dynamically, in the Wicket viewer.

Note that this screencast shows an earlier version of the Wicket viewer UI (specifically, pre 1.8.0).

6.4.2. Required updates to the dom project’s pom.xml

If using the .layout.json files, these must be compiled and available in the classpath. When using an IDE such as Eclipse+M2E, any .layout.json files in src/main/java or src/main/resources will be part of the classpath automatically. However, unless the pom.xml is changed, these will not be part of the compiled WAR.

Therefore, make sure the following is added to the dom project’s pom.xml:

<resources>
    <resource>
        <filtering>false</filtering>
        <directory>src/main/resources</directory>
    </resource>
    <resource>
        <filtering>false</filtering>
        <directory>src/main/java</directory>
        <includes>
            <include>**</include>
        </includes>
        <excludes>
            <exclude>**/*.java</exclude>
        </excludes>
    </resource>
</resources>

If using an Isis SimpleApp archetype, then the POM is already correctly configured.

6.5. Customisation

By default the Wicket viewer will display the application name top-left in the header menu. This can be changed to display a png logo instead.

Screenshots

The screenshot below shows the Isis addons example todoapp (not ASF) with a 'brand logo' image in its header:

brand logo

A custom brand logo (typically larger) can also be specified for the signin page:

brand logo signin
Configuration

In the application-specific subclass of IsisWicketApplication, bind:

  • a string with name "brandLogoHeader" to the URL of a header image. A size of 160x40 works well.

  • a string with name "brandLogoSignin" to the URL of a image for the sign-in page. A size of 400x100 works well.

For example:

@Override
protected Module newIsisWicketModule() {
    final Module isisDefaults = super.newIsisWicketModule();

    final Module overrides = new AbstractModule() {
        @Override
        protected void configure() {
            ...
            bind(String.class).annotatedWith(Names.named("brandLogoHeader"))
                              .toInstance("/images/todoapp-logo-header.png");
            bind(String.class).annotatedWith(Names.named("brandLogoSignin"))
                              .toInstance("/images/todoapp-logo-signin.png");
            ...
        }
    };

    return Modules.override(isisDefaults).with(overrides);
}

If the logo is hosted locally, add to the relevant directory (eg src/main/webapp/images). It is also valid for the URL to be absolute.

You may also wish to tweak the application.css. For example, a logo with height 40px works well with the following:

.navbar-brand img {
    margin-top: -5px;
    margin-left: 5px;
}

6.5.2. Specifying a default theme

The Isis Wicket viewer uses Bootstrap styles and components (courtesy of the Wicket Bootstrap integration).

Unless specified otherwise, the viewer uses the default bootstrap theme. However, this can be changed by overriding init() in the application’s subclass of IsisWicketApplication. For example, to set the bootswatch.com flatly theme as the default, use:

@Override
protected void init() {
    super.init();
    IBootstrapSettings settings = Bootstrap.getSettings();
    settings.setThemeProvider(new BootswatchThemeProvider(BootswatchTheme.Flatly));
}

If you have developed a custom Bootstrap theme (as described here) then this can also be specified using the Wicket Bootstrap API.

6.5.3. Welcome page

It’s possible to customize the application name, welcome message and about message can also be customized. This is done by adjusting the Guice bindings (part of Isis' bootstrapping) in your custom subclass of IsisWicketApplication:

public class MyAppApplication extends IsisWicketApplication {
    @Override
    protected Module newIsisWicketModule() {
        final Module isisDefaults = super.newIsisWicketModule();
        final Module myAppOverrides = new AbstractModule() {
            @Override
            protected void configure() {
                ...
                bind(String.class)
                    .annotatedWith(Names.named("applicationName"))
                     .toInstance("My Wonderful App");
                bind(String.class)
                    .annotatedWith(Names.named("welcomeMessage"))
                    .toInstance(readLines("welcome.html"));         (1)
                bind(String.class)
                    .annotatedWith(Names.named("aboutMessage"))
                    .toInstance("My Wonderful App v1.0");
                ...
            }
        };

        return Modules.override(isisDefaults).with(myAppOverrides);
    }
}
1 the welcome.html file is resolved relative to src/main/webapp.

6.5.4. About page

Isis' Wicket viewer has an About page that, by default, will provide a dump of the JARs that make up the webapp. This page will also show the manifest attributes of the WAR archive itself, if there are any. One of these attributes may also be used as the application version number.

Screenshot

Here’s what the About page looks like with this configuration added:

about page

Note that this screenshot shows an earlier version of the Wicket viewer UI (specifically, pre 1.8.0).

Note that the Build-Time attribute has been used as the version number. The Wicket viewer is hard-coded to search for specific attributes and use as the application version. In order, it searches for:

  • Implementation-Version

  • Build-Time

If none of these are found, then no version is displayed.

Configuration

This configuration is included within the SimpleApp archetype.

Adding attributes to the WAR’s manifest

Add the following to the webapp’s pom.xml (under <build>/<plugins>):

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>build-helper-maven-plugin</artifactId>
    <version>1.5</version>
      <executions>
        <execution>
          <phase>validate</phase>
          <goals>
            <goal>maven-version</goal>
          </goals>
        </execution>
      </executions>
</plugin>

<plugin>
    <artifactId>maven-war-plugin</artifactId>
    <configuration>
        <archive>
            <manifest>
                <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
            </manifest>
            <manifestEntries>
                <Build-Time>${maven.build.timestamp}</Build-Time>
                <Build-Number>${buildNumber}</Build-Number>
                <Build-Host>${agent.name}</Build-Host>
                <Build-User>${user.name}</Build-User>
                <Build-Maven>Maven ${maven.version}</Build-Maven>
                <Build-Java>${java.version}</Build-Java>
                <Build-OS>${os.name}</Build-OS>
                <Build-Label>${project.version}</Build-Label>
            </manifestEntries>
        </archive>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>war</goal>
            </goals>
            <configuration>
                <classifier>${env}</classifier>
            </configuration>
        </execution>
    </executions>
</plugin>

If you then build the webapp from the Maven command line (mvn clean package), then the WAR should contain a META-INF/MANIFEST.MF with those various attribute entries.

Exporting the attributes into the app

The manifest attributes are provided to the rest of the application by way of the Wicket viewer’s integration with Google Guice.

In your subclass of IsisWicketApplication, there is a method newIsisWicketModule(). In this method you need to bind an InputStream that will read the manifest attributes. This is all boilerplate so you can just copy-n-paste:

@Override
protected Module newIsisWicketModule() {
    ...
    final Module simpleappOverrides = new AbstractModule() {
        @Override
        protected void configure() {
            ...
            bind(InputStream.class)
                .annotatedWith(Names.named("metaInfManifest"))
                .toProvider(Providers.of(
                    getServletContext().getResourceAsStream("/META-INF/MANIFEST.MF")));
        }
    };
    ...
}

With that you should be good to go!

6.5.5. Tweaking CSS classes

The HTML generated by the Wicket viewer include plenty of CSS classes so that you can easily target the required elements as required. For example, you could use CSS to suppress the entity’s icon alongside its title. This would be done using:

.entityIconAndTitlePanel a img {
    display: none;
}

These customizations should generally be added to application.css; this file is included by default in every webpage served up by the Wicket viewer.

Targeting individual members

For example, the ToDoItem object of the Isis addons example todoapp (not ASF) has a notes property. The HTML for this will be something like:

<div>
    <div class="property ToDoItem-notes">
        <div class="multiLineStringPanel scalarNameAndValueComponentType">
            <label for="id83" title="">
                <span class="scalarName">Notes</span>
                <span class="scalarValue">
                    <textarea
                        name="middleColumn:memberGroup:1:properties:4:property:scalarIfRegular:scalarValue"
                        disabled="disabled"
                        id="id83" rows="5" maxlength="400" size="125"
                        title="">
                    </textarea>
                    <span>
                    </span>
                </span>
            </label>
       </div>
    </div>
</div>

The application.css file is the place to add application-specific styles. By way of an example, if (for some reason) we wanted to completely hide the notes value, we could do so using:

div.ToDoItem-notes span.scalarValue {
    display: none;
}

You can use a similar approach for collections and actions.

Targeting members through a custom CSS style

The above technique works well if you know the class member to target, but you might instead want to apply a custom style to a set of members. For this, you can use the @CssClass.

For example, in the ToDoItem class the following annotation (indicating that this is a key, important, property) :

@PropertyLayout(cssClass="x-myapp-highlight")
public LocalDate getDueBy() {
    return dueBy;
}

would generate the HTML:

<div>
    <div class="property ToDoItem-dueBy x-myapp-highlight">
        ...
    </div>
</div>

This can then be targeted, for example using:

div.x-myapp-highlight span.scalarName {
    color: red;
}

Note also that instead of using @PropertyLayout(cssClass=…​) annotation, you can also specify the CSS style using a dynamic layout JSON file:

"dueBy": {
    "propertyLayout": {
        "cssClass": "x-myapp-important"
    }
},

6.5.6. Tweaking CSS classes

The HTML generated by the Wicket viewer include plenty of CSS classes so that you can easily target the required elements as required. For example, you could use CSS to suppress the entity’s icon alongside its title. This would be done using:

.entityIconAndTitlePanel a img {
    display: none;
}

These customizations should generally be added to application.css; this file is included by default in every webpage served up by the Wicket viewer.

Targeting individual members

For example, the ToDoItem object of the Isis addons example todoapp (not ASF) has a notes property. The HTML for this will be something like:

<div>
    <div class="property ToDoItem-notes">
        <div class="multiLineStringPanel scalarNameAndValueComponentType">
            <label for="id83" title="">
                <span class="scalarName">Notes</span>
                <span class="scalarValue">
                    <textarea
                        name="middleColumn:memberGroup:1:properties:4:property:scalarIfRegular:scalarValue"
                        disabled="disabled"
                        id="id83" rows="5" maxlength="400" size="125"
                        title="">
                    </textarea>
                    <span>
                    </span>
                </span>
            </label>
       </div>
    </div>
</div>

The application.css file is the place to add application-specific styles. By way of an example, if (for some reason) we wanted to completely hide the notes value, we could do so using:

div.ToDoItem-notes span.scalarValue {
    display: none;
}

You can use a similar approach for collections and actions.

Targeting members through a custom CSS style

The above technique works well if you know the class member to target, but you might instead want to apply a custom style to a set of members. For this, you can use the @CssClass.

For example, in the ToDoItem class the following annotation (indicating that this is a key, important, property) :

@PropertyLayout(cssClass="x-myapp-highlight")
public LocalDate getDueBy() {
    return dueBy;
}

would generate the HTML:

<div>
    <div class="property ToDoItem-dueBy x-myapp-highlight">
        ...
    </div>
</div>

This can then be targeted, for example using:

div.x-myapp-highlight span.scalarName {
    color: red;
}

Note also that instead of using @PropertyLayout(cssClass=…​) annotation, you can also specify the CSS style using a dynamic layout JSON file:

"dueBy": {
    "propertyLayout": {
        "cssClass": "x-myapp-important"
    }
},

6.5.7. Using a different CSS file

If for some reason you wanted to name the CSS file differently (eg stylesheets/myapp.css), then adjust the Guice bindings (part of Isis' bootstrapping) in your custom subclass of IsisWicketApplication:

public class MyAppApplication extends IsisWicketApplication {
    @Override
    protected Module newIsisWicketModule() {
        final Module isisDefaults = super.newIsisWicketModule();
        final Module myAppOverrides = new AbstractModule() {
            @Override
            protected void configure() {
                ...
                bind(String.class)
                    .annotatedWith(Names.named("applicationCss"))
                    .toInstance("stylesheets/myapp.css");
                ...
            }
        };

        return Modules.override(isisDefaults).with(myAppOverrides);
    }
}

As indicated above, this file is resolved relative to src/main/webapp.

6.5.8. Custom Javascript

The Wicket viewer ships with embedded JQuery, so this can be leveraged to perform arbitrary transformations of the rendered page (eg to run some arbitrary JQuery on page load).

Just because something is possible, it doesn’t necessarily mean we encourage it. Please be aware that there is no formal API for any custom javascript that you might implement to target; future versions of Isis might break your code.

If possible, consider using the ComponentFactory API described in the Extending chapter.

To register your Javascript code, adjusting the Guice bindings (part of Isis' bootstrapping) in your custom subclass of IsisWicketApplication:

public class MyAppApplication extends IsisWicketApplication {
    @Override
    protected Module newIsisWicketModule() {
        final Module isisDefaults = super.newIsisWicketModule();
        final Module myAppOverrides = new AbstractModule() {
            @Override
            protected void configure() {
                ...
                bind(String.class)
                    .annotatedWith(Names.named("applicationJs"))
                    .toInstance("scripts/application.js");
                ...
            }
        };
        return Modules.override(isisDefaults).with(myAppOverrides);
    }
}

Currently only one such .js file can be registered.

6.5.9. Auto-refresh page

This requirement from the users mailing list:

Suppose you want to build a monitoring application, eg for an electricity grid. Data is updated in the background (eg via the Restful Objects REST API). What is needed is the ability to show an entity that includes a map, and have it auto-refresh every 5 seconds or so.

Here’s one (somewhat crude, but workable) way to accomplish this.

  • First, update the domain object to return custom CSS:

    public class MyDomainObject {
        ...
        public String cssClass() {return "my-special-auto-updating-entity"; }
        ...
    }
  • Then, use javascript in webapp/src/main/webapp/scripts/application.js to reload:

    $(function() {
        if ($(".my-special-auto-updating-entity").length) {
            setTimeout(function() {document.location.reload();}, 5000); // 1000 is 5 sec
        }
    });

6.6. Extending the viewer

APIs to extend the Wicket viewer are discussed in the Extending chapter.

6.7. Isis Add-ons (not ASF)

The (non-ASF) Isis Addons website provides a number of extensions to the Wicket viewer (leveraging the APIs described in [Extending the Wicket viewer] section, later. While you are free to fork and adapt any of them to your needs, they are also intended for use "out-of-the-box".

Note that Isis addons, while maintained by Isis committers, are not part of the ASF.

This short section gives an overview of their capabilities. For full information, check out the README on their respective github repo. Each wicket extension also includes a demo app so you can (a) see what the extension does and (b) see how to use it within your own code.

6.7.1. Excel download

TODO

6.7.2. Fullcalendar2

TODO

6.7.3. Gmap3

TODO

6.7.4. Wicked charts

TODO

7. Restful Objects Viewer

Apache Isis' Restful Objects viewer is an implementation of the Restful Objects spec, which defines a generic way to expose a domain model through a REST (or more precisely, hypermedia) API.

The Restful Objects viewer also provides a number of proprietary extensions.

7.1. Features

The REST API opens up an Isis domain model to a huge variety of applications, from bespoke single-page apps, through integration scenarious, through providing an API for bulk-upload/migration from an existing system.

7.1.1. Restful Objects Specification

TODO - a brief summary here

The Restful Objects spec can be downloaded here.

7.1.2. Pretty printing

The JSON representations generated by the Restful Objects viewer are in compact form if the deployment type is SERVER (ie production), but will automatically be "pretty printed" (in other words indented) if the deployment type is PROTOTYPE.

7.2. Configuration Properties

The Restful Objects viewer provides a number of configuration option that extend/simplify/alter the representations generated from the Restful Objects specification.

These configuration properties are typically stored in WEB-INF/viewer_restfulobjects.properties. However, you can place all configuration properties into WEB-INF/isis.properties if you wish (the configuration properties from all config files are merged together).

These configuration settings should be considered beta, and may change in the future in response to emerging requirements.

Also, be aware enabling these settings makes the representations with respect to the Restful Object spec. (Based on experience in Isis, in the future the spec may be updated to allow such extensions).

Table 3. Restful Objects Viewer Configuration Properties
Property Value
(default value)
Description

isis.viewer.restfulobjects.
honorUiHints

true,false (false)

See discussion, below.

isis.viewer.restfulobjects.
suppressDescribedByLinks

true,false (false)

See discussion, below.

isis.viewer.restfulobjects.
suppressUpdateLink

true,false (false)

isis.viewer.restfulobjects.
suppressMemberId

true,false (false)

isis.viewer.restfulobjects.
suppressMemberLinks

true,false (false)

isis.viewer.restfulobjects.
suppressMemberExtensions

true,false (false)

isis.viewer.restfulobjects.
suppressMemberDisabledReason

true,false (false)

isis.viewer.restfulobjects.
objectPropertyValuesOnly

true,false (false)

See discussion, below.

7.2.1. Honor UI hints

By default the representations generated by Restful Objects ignore any Isis metamodel hints referring to the UI. In particular, if a collection is annotated then Render(EAGERLY) then the contents of the collection are not eagerly embedded in the object representation.

However, this behaviour can be overridden globally using following property (typically added to WEB-INF/viewer_restfulobjects.properties):

isis.viewer.restfulobjects.honorUiHints=true

In the future we might extend this per-request. Raise/vote on a JIRA ticket if you require this feature.

7.2.2. Suppressing elements of the representation

The representations specified by the Restful Object spec are very rich in hypermedia controls and metadata, intended to support a wide variety of possible REST clients. However, if an application is providing its REST API only for a small well-defined set of REST clients, then it is possible to suppress (remove) various elements of these representations.

This is done by globally using the following properties (typically added to WEB-INF/viewer_restfulobjects.properties):

isis.viewer.restfulobjects.suppressDescribedByLinks=true      (1)
isis.viewer.restfulobjects.suppressUpdateLink=true            (2)
isis.viewer.restfulobjects.suppressMemberId=true              (3)
isis.viewer.restfulobjects.suppressMemberLinks=true           (4)
isis.viewer.restfulobjects.suppressMemberExtensions=true      (5)
isis.viewer.restfulobjects.suppressMemberDisabledReason=true  (6)
1 suppresses the "describedby" links (on all representations)
2 suppresses the "update" link (on object representation)
3 suppresses the "id" json-prop for object members (on object representation and member detail representations)
4 suppresses the "links" json-prop for object members (on the object representation and member detail representations)
5 suppresses the "extensions" json-prop for object members (on the object representation and member detail representations)
6 suppresses the "disabledReason" json-prop for object members (on the object representation and member detail representations)

The defaults for all of these is false, meaning that the hypermedia/metadata is NOT suppressed.

In the future we might extend this per-request. Raise/vote on a JIRA ticket if you require this feature.

If an even simpler representations (of objects) are required, see simplified object representation, immediately below. And if the above isn’t flexible and you need complete control over all representations see the section on Extending the Restful Objects viewer in the Extending chapter.

7.2.3. Simplified object representation

The representations specified by the Restful Object spec are very rich in hypermedia controls and metadata, intended to support a wide variety of possible REST clients.

As described above, it is possible to suppress various elements of these representations. Even then, though, the representations may be too complex for some bespoke REST clients that require a very "flat" object representation.

The Restful Objects viewer therefore supports generating a much simpler representation of objects using the following configuration property (typically added to WEB-INF/viewer_restfulobjects.properties):

isis.viewer.restfulobjects.objectPropertyValuesOnly=true

This generates a representation such as:

{
    "title" : "Buy milk due by 2014-10-27",
    "domainType" : "TODO",
    "instanceId" : "0",
    "members" : {
        "description" : "Buy milk",
        "category" : "Domestic",
        "subcategory" : "Shopping",
        "complete" : false,
        "versionSequence" : 1,
        "relativePriority" : 2,
        "dueBy" : "2014-10-27",
        "cost" : "0.75",
        "notes" : null,
        "attachment" : null,
        "doc" : null
    },
    "links" : [
        {
            "rel" : "self",
            "href" : "http://localhost:8080/restful/objects/TODO/0",
            "method" : "GET",
            "type" : "application/json;profile=\"urn:org.restfulobjects:repr-types/object\"",
            "title" : "Buy milk due by 2014-10-27"
        },
        {
            "rel" : "describedby",
            "href" : "http://localhost:8080/restful/domain-types/TODO",
            "method" : "GET",
            "type" : "application/json;profile=\"urn:org.restfulobjects:repr-types/domain-type\""
        }
    ],
    "extensions" : {
        "oid" : "TODO:0"
    },
}

In the future we might extend this per-request. Raise/vote on a JIRA ticket if you require this feature.

If the above isn’t flexible and you need complete control over all representations see the section on Extending the Restful Objects viewer.

7.3. Hints and Tips

Since the Restful Objects viewer is designed for computer programs to interact with (rather than human beings), it can be a little difficult to explore and generally "grok" how it works.

This section provides a few hints-and-tips to help you on your way.

7.3.1. Using Chrome Tools

The screencast below shows how to explore the Restful API using Chrome plugins/extensions, and how we use them to write end-2-end (TCK) tests for the Restful Objects viewer.

7.3.2. AngularJS Tips

The hypermedia API exposed by Isis' Restful Objects viewer is intended be support both bespoke custom-written viewers as well as generic viewers. Indeed, we expect most clients consuming the API will be bespoke, not generic.

This page captures one or two tips on using AngularJS to write such a bespoke client.

Suppose you have a CustomerService providing a findCustomer action:

public class CustomerService {
  public String id() { return "customers"; }
  @ActionSemantics(Of.SAFE)
  public Customer findCustomer(@Named("customerName") String customerName) { ... }
}

Restful Objects will expose this as action with the following link that looks something like:

{
  "rel" : "urn:org.restfulobjects:rels/invoke",
  "href" : "http://localhost:8080/restful/services/customers/actions/findCustomer/invoke",
  "method" : "GET",
  "type" : "application/json;profile=\"urn:org.restfulobjects:repr-types/action-result\"",
  "arguments" : {
    "customerName" : {
      "value" : null
    }
  }
}

You can then invoke this using AngularJs' $resource service as follows.

var findCustomer = $resource("http://localhost:8080/restful/services/customers/actions/findCustomer/invoke?:queryString");
var findCustomerArgs = {
  "customerName": {
      "value": "Fred"
    }
};
findCustomer.get({queryString: JSON.stringify(findCustomerArgs)}, function(data) { ... } )

Here the :queryString placeholder in the initial $resource constructor is expanded with a stringified version of the JSON object representing the args. Note how the findCustomerArgs is the same as the "arguments" attribute in the original link (with a value provided instead of null).

Invoking a PUT or POST link

If the method is a PUT or a POST, then no :queryString placeholder is required in the URL, and the args are instead part of the body.

Use $resource.put(…​) or $resource.post(…​) instead.

7.4. Extending the viewer

8. Security

Apache Isis has built-in support for authentication and authorization:

  • By "authentication" we mean logging into the application using some credentials, typically a username and password. Authentication also means looking up the set of roles to which a user belongs.

  • By "authorization" we mean permissions: granting roles to have access to features (object member) of the app.

Isis has two levels of permissions. Read permission means that the user can view the object member; it will be rendered in the UI. An action with only read permission will be shown disabled ("greyed out". Write permission means that the object member can be changed. For actions this means that they can be invoked.

Isis provides an API for both authentication and authorization, and provides an implementation that integrates with Apache Shiro. Shiro in turn uses the concept of a realm as a source for both authentication and optionally authorization.

Shiro ships with a simple text-based realm — the IniRealm — which reads users (and password), user roles and role permissions from the WEB-INF/shiro.ini file. The SimpleApp archetype is configured to use this realm.

Shiro also ships with an implementation of an LDAP-based realm; LDAP is often used to manage user/passwords and corresponding user groups. Isis in turn extends this with its IsisLdapRealm, which provides more flexibility for both group/role and role/permissions management.

In addition, the (non-ASF) Isis Addons provides the Isis addons' security module, which also provides an implementation of the Shiro Realm API. However, the security module also represents users, roles and permissions as domain objects, allowing them to be administered through Isis itself. Moreover, the security module can optionally delegate password management to a subsidiary (delegate) realm (usually LDAP as discussed above).

In addition to Isis' Shiro-based implementation of its authentication and authorization APIs, Isis also provides a "bypass" implementation, useful for quick-n-dirty prototyping when you want to in effect disable (bypass) security completely.

What about auditing?

A further aspect of security is auditing: recording what data was modified by which user. Apache Isis does define two service APIs — CommandService and AuditingService — that provide hooks to track which actions (commands) are invoked, and what data was modified as a result (auditing).

Isis does not itself provide any in-built implementations of these APIs, but there are implementations within (non-ASF) Isis Addons, namely the Isis addons' command module and the Isis addons' auditing module.

8.1. Configuring Isis to use Shiro

Apache Isis' security mechanism is configurable, specifying an Authenticator and an Authorizor (non-public) APIs. The Shiro security mechanism is an integration wih Apache Shiro that implements both interfaces.

The SimpleApp archetype is pre-configured to use Apache Shiro, so much of what follows is set up already.

8.1.1. Telling Isis to use Shiro

To tell Apache Isis to use Shiro, update the WEB-INF/isis.properties file:

isis.authentication=shiro
isis.authorization=shiro

This installs the appropriate implementation (the ShiroAuthenticatorOrAuthorizor class) that use Shiro’s APIs to perform authentication and authorization:

configure isis to use shiro

The figure above doesn’t tell the whole story; we haven’t yet seen how Shiro itself is configured to use realms. The ShiroAuthenticatorOrAuthorizor is in essence the glue between the Isis runtime and Shiro.

8.1.2. Bootstrapping Shiro

The Shiro environment (in essence, thread-locals holding the security credentials) needs to be bootstrapped using the following settings in the WEB-INF/web.xml file:

<listener>
    <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class>
</listener>
<filter>
    <filter-name>ShiroFilter</filter-name>
    <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>ShiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Based on this Shiro will then read WEB-INF/shiro.ini file to configure its Realm definitions for authentication and authorization.

8.1.3. WEB-INF/shiro.ini

The shiro.ini file is used to specify the realm(s) that Shiro will delegate to:

securityManager.realms = $realmName

Shiro’s ini file supports a "poor-man’s" dependency injection (their words), and so $realmName in the above example is a reference to a realm defined elsewhere in shiro.ini. The subsequent sections describe the specifics for thevarious realm implementations available to you.

It’s also possible to configure Shiro to support multiple realms.

securityManager.realms = $realm1,$realm2

You can learn more about Shiro realms in the Shiro documentation.

8.2. Shiro Ini Realm

Probably the simplest realm to use is Shiro’s built-in IniRealm, which reads from the (same) WEB-INF/shiro.ini file.

This is suitable for prototyping, but isn’t intended for production use, if only because user/password credentials are stored in plain text. Nevertheless, it’s a good starting point. The app generated by the SimpleApp archetype is configured to use this realm.

The diagram below shows the Isis and components involved:

configure shiro to use ini realm

The realm is responsible for validating the user credentials, and then creates a Shiro Subject which represents the user (for the current request). Isis Authenticator component then interacts with the Subject in order to check permissions.

8.2.1. Shiro Configuration

To use the built-in IniRealm, we add the following to WEB-INF/shiro.ini:

securityManager.realms = $iniRealm

(Unlike other realms) there is no need to "define" $iniRealm; it is automatically available to us.

Specifying $iniRealm means that the usernames/passwords, roles and permissions are read from the shiro.ini file itself. Specifically:

  • the users/passwords and their roles from the [users] sections;

  • the roles are mapped to permissions in the [roles] section.

The format of these is described below.

[users] section

This section lists users, passwords and their roles.

For example:

sven = pass, admin_role
dick = pass, user_role, analysis_role, self-install_role
bob  = pass, user_role, self-install_role

The first value is the password (eg "pass", the remaining values are the role(s).

[roles] section

This section lists roles and their corresponding permissions.

For example:

user_role = *:ToDoItems:*:*,\
            *:ToDoItem:*:*,\
            *:ToDoAppDashboard:*:*
analysis_role = *:ToDoItemAnalysis:*:*,\
            *:ToDoItemsByCategoryViewModel:*:*,\
            *:ToDoItemsByDateRangeViewModel:*:*
self-install_role = *:ToDoItemsFixturesService:install:*
admin_role = *

The value is a comma-separated list of permissions for the role. The format is:

packageName:className:memberName:r,w

where:

  • memberName is the property, collection or action name.

  • r indicates that the member is visible

  • w indicates that the member is usable (editable or invokable)

and where each of the parts of the permission string can be wildcarded using *.

Because these are wildcards, a '*' can be used at any level. Additionally, missing levels assume wildcards.

Thus:

com.mycompany.myapp:Customer:firstName:r,w   # view or edit customer's firstName
com.mycompany.myapp:Customer:lastName:r      # view customer's lastName only
com.mycompany.myapp:Customer:placeOrder:*    # view and invoke placeOrder action
com.mycompany.myapp:Customer:placeOrder      # ditto
com.mycompany.myapp:Customer:*:r             # view all customer class members
com.mycompany.myapp:*:*:r                    # view-only access for all classes in myapp package
com.mycompany.myapp:*:*:*                    # view/edit for all classes in myapp package
com.mycompany.myapp:*:*                      # ditto
com.mycompany.myapp:*                        # ditto
com.mycompany.myapp                          # ditto
*                                            # view/edit access to everything

The format of the permissions string is configurable in Shiro, and Isis uses this to provide an extended wildcard format, described here.

8.2.2. Externalized IniRealm

There’s no requirement for all users/roles to be defined in the shiro.ini file. Instead, a realm can be defined that loads its users/roles from some other resource.

For example:

$realm1=org.apache.shiro.realm.text.IniRealm (1)
realm1.resourcePath=classpath:webapp/realm1.ini (2)
1 happens to (coincidentally) be the same implementation as Shiro’s built-in $iniRealm
2 in this case load the users/roles from the src/main/resources/webapp/realm1.ini file.

Note that a URL could be provided as the resourcePath, so a centralized config file could be used. Even so, the

If configured this way then the [users] and [roles] sections of shiro.ini become unused. Instead, the corresponding sections from for realm1.ini are used instead.

8.3. Isis Ldap Realm

Isis ships with an implementation of Apache Shiro's Realm class that allows user authentication and authorization to be performed against an LDAP server.

configure shiro to use isis ldap realm

The LDAP database stores the user/passwords and user groups, while the shiro.ini file is used to map the LDAP groups to roles, and to map the roles to permissions.

8.3.1. Shiro Configuration

To use LDAP involves telling Shiro how to instantiate the realm. This bootstrapping info lives in the WEB-INF/shiro.ini:

contextFactory = org.apache.isis.security.shiro.IsisLdapContextFactory
contextFactory.url = ldap://localhost:10389
contextFactory.systemUsername = uid=admin,ou=system        (1)
contextFactory.systemPassword = secret
contextFactory.authenticationMechanism = CRAM-MD5          (2)
contextFactory.systemAuthenticationMechanism = simple

ldapRealm = org.apache.isis.security.shiro.IsisLdapRealm   (3)
ldapRealm.contextFactory = $contextFactory

ldapRealm.searchBase = ou=groups,o=mojo                    (4)
ldapRealm.groupObjectClass = groupOfUniqueNames            (5)
ldapRealm.uniqueMemberAttribute = uniqueMember             (6)
ldapRealm.uniqueMemberAttributeValueTemplate = uid={0}

# optional mapping from physical groups to logical application roles
ldapRealm.rolesByGroup = \                                 (7)
    LDN_USERS: user_role,\
    NYK_USERS: user_role,\
    HKG_USERS: user_role,\
    GLOBAL_ADMIN: admin_role,\
    DEMOS: self-install_role

ldapRealm.permissionsByRole=\                              (8)
   user_role = *:ToDoItemsJdo:*:*,\
               *:ToDoItem:*:*; \
   self-install_role = *:ToDoItemsFixturesService:install:* ; \
   admin_role = *

securityManager.realms = $ldapRealm
1 user accounts are searched using a dedicated service account
2 SASL (CRAM-MD5) authentication is used for this authentication
3 Isis' implementation of the LDAP realm.
4 groups are searched under ou=groups,o=mojo (where mojo is the company name)
5 each group has an LDAP objectClass of groupOfUniqueNames
6 each group has a vector attribute of uniqueMember
7 groups looked up from LDAP can optionally be mapped to logical roles; otherwise groups are used as role names directly
8 roles are mapped in turn to permissions

The value of uniqueMember is in the form uid=xxx, with xxx being the uid of the user * users searched under ou=system * users have, at minimum, a uid attribute and a password * the users credentials are used to verify their user/password

The above configuration has been tested against ApacheDS, v1.5.7. This can be administered using Apache Directory Studio, v1.5.3.

Shiro Realm Mappings

When configuring role based permission mapping, there can only be one of these entries per realm:

realm.groupToRolesMappings = ...

and

realm.roleToPermissionsMappings = ...

This forces you to put everything on one line for each of the above. This is, unfortunately, a Shiro "feature". And if you repeat the entries above then it’s "last one wins".)

To make the configuration maintainable, use "\" to separate the mappings onto separate lines in the file. Use this technique for both group to roles mapping and role to permission mapping. If you use the '' after the "," that separates the key:value pairs it is more readable.

8.3.2. Externalizing role perms

As an alternative to injecting the permissionsByRole property, the role/permission mapping can alternatively be specified by injecting a resource path:

ldapRealm.resourcePath=classpath:webapp/myroles.ini

where myroles.ini is in src/main/resources/webapp, and takes the form:

[roles]
user_role = *:ToDoItemsJdo:*:*,\
            *:ToDoItem:*:*
self-install_role = *:ToDoItemsFixturesService:install:*
admin_role = *

This separation of the role/mapping can be useful if Shiro is configured to support multiple realms (eg an LdapRealm based one and also an TextRealm)

8.3.3. Active DS LDAP tutorial

The screenshots below show how to setup LDAP accounts in ApacheDS using the Apache Directory Studio.

The setup here was initially based on this tutorial, however we have moved the user accounts so that they are defined in a separate LDAP node.

To start, create a partition in order to hold the mojo node (holding the groups):

ActiveDS LDAP Users

Create the ou=groups,o=mojo hierarchy:

ActiveDS LDAP Users

Configure SASL authentication. This means that the checking of user/password is done implicitly by virtue of Isis connecting to LDAP using these credentials:

ActiveDS LDAP Users

In order for SASL to work, it seems to be necessary to put users under o=system. (This is why the setup is slightly different than the tutorial mentioned above):

ActiveDS LDAP Users

Configure the users into the groups:

ActiveDS LDAP Users

8.4. Security Module Realm

The Isis Addons' security module (not ASF) provides a complete security subdomain for users, roles, permissions; all are persisted as domain entities.

What that means, of course, that they can also be administered through your Isis application. Moreover, the set of permissions (to features) is derived completely from your application’s metamodel; in essence the permissions are "type-safe".

In order to play along, the module includes a Shiro realm, which fits in as follows:

The general configuration is as follows:

configure shiro to use isisaddons security module realm

where the IsisModuleSecurityRealm realm is the implementation provided by the module.

In the configuration above user passwords are stored in the database. The module uses jBCrypt so that passwords are only stored in a (one-way) encrypted form in the database.

The security module also supports a slightly more sophisticated configuration. Most organizations use LDAP for user credentials, and maintaining two separate user accounts would be less than ideal. The IsisModuleSecurityRealm can therefore be configured with a subsidiary "delegate" realm that is responsible for performing the primary authentication of the user; if that passes then a user is created (as a domain entity) automatically. In most cases this delegate realm will be the LDAP realm, and so the architecture becomes:

configure shiro to use isisaddons security module realm with delegate realm

The security module has many more features than are described here, all of which are described in the module’s README. The README also explains in detail how to configure an existing app to use this module.

You can also look at the Isisaddons todoapp example (not ASF), which is preconfigured to use the security module.

8.5. Shiro JDBC Realm

There is nothing to stop you from using some other Realm implementation (or indeed writing one yourself). For example, you could use Shiro’s own JDBC realm that loads user/password details from a database.

If you are happy to use a database then we strongly recommend you use the Isis addons' security module instead of a vanilla JDBC; it is far more sophisticated and moreover gives you the ability to administer the system from within your Isis application.

If you go down this route, then the architecture is as follows:

configure shiro to use custom jdbc realm

There’s quite a lot of configuration required (in WEB-INF/shiro.ini) to set up a JDBC realm, so we’ll break it out into sections.

First, we need to set up the connection to JDBC:

jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm        (1)

jof = org.apache.shiro.jndi.JndiObjectFactory          (2)
jof.resourceName = jdbc/postgres                       (3)
jof.requiredType = javax.sql.DataSource
jof.resourceRef = true

jdbcRealm.dataSource = $jof                            (4)
1 instantiate the JDBC realm
2 instantiate factory object to lookup DataSource from servlet container
3 name of the datasource (as configured in web.xml)
4 instruct JDBC realm to obtain datasource from the JNDI

We next need to tell the realm how to query the database. Shiro supports any schema; what matters is the input search argument and the output results.

jdbcRealm.authenticationQuery =         \              (1)
        select password                 \
          from users                    \
         where username = ?

jdbcRealm.userRolesQuery =              \              (2)
        select r.label                  \
          from users_roles ur           \
    inner join roles r                  \
            on ur.role_id = r.id        \
         where user_id = (              \
            select id                   \
             from users                 \
            where username = ?);        \

jdbcRealm.permissionsQuery=             \               (3)
        select p.permission             \
          from roles_permissions rp     \
    inner join permissions p            \
            on rp.permission_id = p.id  \
         where rp.role_id = (           \
            select id                   \
             from roles                 \
            where label = ?);

jdbcRealm.permissionsLookupEnabled=true                 (4)
1 query to find password for user
2 query to find roles for user
3 query to find permissions for role
4 enable permissions lookup

The permissionsLookupEnabled is very important, otherwise Shiro just returns an empty list of permissions and your users will have no access to any features(!).

We also should ensure that the passwords are not stored as plain-text:

dps = org.apache.shiro.authc.credential.DefaultPasswordService   (1)
pm = org.apache.shiro.authc.credential.PasswordMatcher           (2)
pm.passwordService = $dps
jdbcRealm.credentialsMatcher = $pm                               (3)
1 mechanism to encrypts password
2 service to match passwords
3 instruct JDBC realm to use password matching service when authenticating

And finally we need to tell Shiro to use the realm, in the usual fashion:

securityManager.realms = $jdbcRealm

Using the above configuration you will also need to setup a DataSource. The details vary by servlet container, for example this is how to do the setup on Tomcat 8.0.

The name of the DataSource can also vary by servlet container; see for example this StackOverflow answer.

8.6. Enhanced Wildcard Permission

If using the text-based IniRealm or Isis' LDAP realm, then note that Shiro also allows the string representation of the permissions to be mapped (resolved) to alternative Permission instances. Isis provides its own IsisPermission which introduces the concept of a "veto".

A vetoing permission is one that prevents access to a feature, rather than grants it. This is useful in some situations where most users have access to most features, and only a small number of features are particularly sensitive. The configuration can therefore be set up to grant fairly broad-brush permissions and then veto permission for the sensitive features for those users that do not have access.

The string representation of the IsisPermission uses the following format:

([!]?)([^/]+)[/](.+)

where:

  • the optional ! prefix indicates this permission is a vetoing permission

  • the optional xxx/ prefix is a permission group that scopes any vetoing permissions

  • the remainder of the string is the permission (possibly wildcarded, with :rw as optional suffix)

For example:

user_role   = !reg/org.estatio.api,\
              !reg/org.estatio.webapp.services.admin,\
              reg/* ; \
api_role    = org.estatio.api ;\
admin_role = adm/*

sets up:

  • the user_role with access to all permissions except those in org.estatio.api and org.estatio.webapp.services.admin

  • the api_role with access to all permissions in org.estatio.api

  • the admin_role with access to everything.

The permission group concept is required to scope the applicability of any veto permission. This is probably best explained by an example. Suppose that a user has both admin_role and user_role; we would want the admin_role to trump the vetos of the user_role, in other words to give the user access to everything.

Because of the permission groups, the two "!reg/...+""" vetos in user_role only veto out selected permissions granted by the "reg/" permissions, but they do not veto the permissions granted by a different scope, namely "adm/+".

The net effect is therefore what we would want: that a user with both admin_role and user_role would have access to everything, irrespective of those two veto permissions of the user_role.

Finally, the Isis permission resolver is specified in WEB-INF/shiro.ini file:

permissionResolver = org.apache.isis.security.shiro.authorization.IsisPermissionResolver
myRealm.permissionResolver = $permissionResolver  (1)
1 myRealm is the handle to the configured realm, eg $iniRealm or $isisLdapRealm etc.

8.7. Bypassing security

The bypass security component consists of an implementation of both the AuthenticationManager and AuthorizationManager APIs, and are intended for prototyping use only.

The authentication manager allows access with any credentials (in a sense, "bypassing" authentication), while the authorization manager provides access to all class members (in a sense, "bypassing" authorization).

To tell Apache Isis to bypass security, just update the WEB-INF/isis.properties file:

isis.authentication=bypass
isis.authorization=bypass

This installs the appropriate no-op implementations for both authentication and authorization:

configure isis to use bypass

8.8. API for Applications

Generally speaking your domain objects (or more generally your application) should be agnostic of the user/roles that are interacting with them; applying security permissions is the responsibility of the framework.

Still, on very rare occasion you may have a need, in which case you can either use Isis' DomainObjectContainer API or you can reach further down the stack and use the JEE Servlet API.

8.8.1. DomainObjectContainer API

The DomainObjectContainer service exposes the following API:

final UserMemento user = container.getUser();
final List<RoleMemento> roles = user.getRoles();
for (RoleMemento role : roles) {
    String roleName = role.getName();
    ...
}

Each role’s name property encodes both the realm that provided the role, and the role identity itself.

For example, in the simpleapp, if logging in as dick with the following entries for realm1:

dick = pass, user_role, analysis_role, self-install_role

then this corresponds to the roles "realm1:user_role", "realm1:self-install_role" and "realm1:analysis_role".

If using the Wicket viewer, then note there will also be another role which is used internally (namely "org.apache.isis.viewer.wicket.roles.USER").

8.8.2. Servlet API

On occasion you may find it necessary to reach below Isis and to the underlying servlet API. For example, the Isis addons' togglz module (non-ASF) has a requirement to do this in order to expose its embedded togglz web console. (

If you do need to access the servlet API and are running within the context of Wicket viewer, the Isis addons' servletapi module can provide access to the HttpServletRequest, HttpServletResponse and ServletContext.

In this situation, you can still obtain some information about the user and its roles:

Principal principal = httpServletRequest.getPrincipal();
String username = principal.getName();
boolean analyst = httpServletRequest.isUserInRole("analysis_role");

However, it isn’t possible to obtain any role permissions for the user.

8.9. Usage by Isis' Viewers

By and large the security mechanisms within Isis are transparent to the rest of the framework (the Wicket Viewer and Restful Objects viewer, and the overall runtime).

That said, it is the responsibility of the viewers to ensure that there is a viewers to ensure that for each request there is a valid user session present. The sections below explain how this works.

8.9.1. Wicket Viewer

The Wicket viewer defines a relatively small number of pages (by which we mean subclasses of org.apache.wicket.markup.html.WebPage):

  • about page

  • action prompt page

  • entity page

  • error page

  • home page

  • standalone collection page

  • value page

  • void return page

All of these (except about page) are annotated with the Wicket annotation:

@AuthorizeInstantiation("org.apache.isis.viewer.wicket.roles.USER")

which means that they can only be accessed by a user with an authenticated session that has this special, reserved role. If not, Wicket will automatically redirect the user to the sign-in page.

The sign-in page to render is pluggable; see extensions chapter for details.

In the sign-in page the viewer calls to the Isis Authenticator API, and obtains back a user/role. It also adds in its special reserved role (per the annotation above) and then continues on to whichever page the user was attempting to access (usually the home page).

And that’s really all there is to it. When the viewer renders a domain object it queries the Isis metamodel, and suppresses from the view any object members (properties, actions etc) that are invisible. These may be invisible because the user has no (read ) permission, or they may be invisible because of domain object logic (eg a hideXxx() method). The viewer neither knows nor cares.

Similarly, for those object members that are visible, the viewer also checks if they are enabled or disabled. Again, an object member will be disabled if the user does not have (write) permission, or it could be disabled because of domain object logic (eg a disableXxx() method).

User-registration

As well as providing a sign-in screen, the Wicket viewer also provides the ability for users to self-register. By and large this operates outside of Isis' security mechanisms; indeed the various pages (sign-up, sign-up verification, password reset) are all rendered without there being any current user session. These pages all "reach inside" Isis framework using a mechanism similar to [_headless_access] in order to actually do their stuff.

The sign-in verification page to render is pluggable; see extensions chapter for details.

User registration is only available if the UserRegistrationService is configured; this is used by the framework to actually create new instances of the user as accessed by the corresponding (Shiro) realm.

Because Shiro realms are pluggable, the Isis framework does not provide default implementations of this service. However, if you are using the Isis addons' security module (non-ASF), then this module does provide an implementation (that, as you might expect, creates new "user" domain entities).

If you are using an LDAP realm and want to enable user-self registration then you’ll need to write your own implementation of this service.

8.9.2. Restful Objects Viewer

Unlike the Wicket viewer, the Restful Objects viewer does _not provide any sort of login page; rather it provides a pluggable authentication strategy, delegated to by the IsisSessionFilter filter defined in web.xml. The authentication strategy is responsible for ensuring that a session is available for the REST resource.

The API of AuthenticationSessionStrategy is simply:

package org.apache.isis.core.webapp.auth;
...
public interface AuthenticationSessionStrategy {
    AuthenticationSession lookupValid(     (1)
        ServletRequest servletRequest,
        ServletResponse servletResponse);
    void bind(                             (2)
        ServletRequest servletRequest,
        ServletResponse servletResponse,
        AuthenticationSession authSession);
}
1 returns a valid AuthenticationSession for the specified request, response
2 binds (associates the provided AuthenticationSession) to the request and response

Here AuthenticationSession is Isis' internal API that represents a signed-on user.

The framework provides a number of simple strategies:

  • AuthenticationSessionStrategyBasicAuth implements the HTTP basic auth protocol (the pop-up dialog box shown by the web browser)

  • AuthenticationSessionStrategyHeader that simply reads the user identity from an HTTP header

  • AuthenticationSessionStrategyTrusted that always logs in with a special "exploration" user

As you can see, none of these should be considered production-quality.

The strategy is configured in web.xml; for example:

<filter>
    <filter-name>IsisSessionFilterForRestfulObjects</filter-name>
    <filter-class>org.apache.isis.core.webapp.IsisSessionFilter</filter-class>
    <init-param>
        <param-name>authenticationSessionStrategy</param-name>
        <param-value>                                           (1)
    org.apache.isis.viewer.restfulobjects.server.authentication.AuthenticationSessionStrategyBasicAuth
        </param-value>
    </init-param>
    <init-param>
        <param-name>whenNoSession</param-name>
        <param-value>basicAuthChallenge</param-value>           (2)
    </init-param>
</filter>
1 configure basic auth strategy
2 what to do if no session was found; we indicate to issue a 401 basic authentication challenge

The IsisSessionFilter has a further plugin point if no session was found (that is, if AuthenticationSessionStrategy#lookupValid() returns null), which is how to respond. The value "basicAuthChallenge" means to return a 401, which is what triggers the web browser to pop-up the basic HTTP challenge dialog box.

The above filter must then be chained before the servlet that actually handles the REST requests:

<filter-mapping>
    <filter-name>IsisSessionFilterForRestfulObjects</filter-name>
    <servlet-name>RestfulObjectsRestEasyDispatcher</servlet-name>
</filter-mapping>
...
<servlet>
    <servlet-name>RestfulObjectsRestEasyDispatcher</servlet-name>
    <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
</servlet>

The above web.xml fragments do not constitute the full configuration for the Restful Objects viewer, just those parts that pertain to security.

User-registration

Isis currently does not have any out-of-the-box support for user-registration for applications using only the Restful viewer. However, in principal the pieces do exist to put together a solution.

The general idea is similar to the design of the Wicket viewer; define some subsidiary resources that can operate without a user session in place, and which "reach into" the framework using headless access in order to setup the user.

An alternative approach, possibly less work and overall of more value, would be to implement AuthenticationSessionStrategy for oAuth, in other words allow users to use their existing Google or Facebook account.

The following steps sketch out the solution in a little more detail:

  • Define some new Restful resources (cf DomainServiceResourceServerside that correspond to sign-up/register page, eg SignupResource

    @Path("/signup")
    public class SignupResource {
        ...
    }
  • Create a new subclass of RestfulObjectsApplication, eg "CustomRestfulObjectsApplication" and register your resources

    public class CustomRestfulObjectsApplication extends RestfulObjectsApplication {
        public CustomRestfulObjectsApplication() {
            addClass(SignupResource.class);
        }
    }
  • Register your application class in web.xml instead of the default:

    <context-param>
        <param-name>javax.ws.rs.Application</param-name>
        <param-value>com.mycompany.myapp.CustomRestfulObjectsApplication</param-value>
    </context-param>

So far this is just standard javax.rs stuff.

Next, we need to ensure that a client can hit your new resource with the Isis runtime in place, but without there being an Isis session. For that…​.

  • create a subclass of the AuthenticationSessionStrategy that automatically returns a dummy session if the resource being accessed is "/restful/signup", say.

    You could do this by subclassing AuthenticationSessionStrategyBasicAuth, but then using code from AuthenticationSessionStrategyBasicAuth to return an "exploration" (or better, "signup") session if accessing the "/restful/signup" resource.

  • in the SignUpResource resource, you can then do a lookup of the UserRegistrationService in order to allow the user to be created:

    final UserRegistrationService userRegistrationService =
        IsisContext.getPersistenceSession().getServicesInjector().lookupService(UserRegistrationService.class);
    userRegistrationService.registerUser(userDetails);

Obviously the methods exposed by the SignupResource are up to you; ultimately they need to be able to gather information to populate the UserDetails object as passed to the UserRegistrationService.

9. Runtime

Isis' own configuration properties are simple key-value pairs, typically held in the WEBINF/isis.properties file and other related files. This chapter describes how to configure an Apache Isis application.

Configuration properties for the viewers can be found in the Wicket Viewer chapter and the Restful Objects Viewer chapter. Likewise[details of configuring security (Apache Shiro) can be found in the Security chapter.

Also, note that by default the configuration values are part of the built WAR file. Details on how to override these configuration properties externally for different environments can be found in the Deployment chapter.

9.1. Deployment Types

9.2. Configuration Files

When running an Isis webapp, configuration properties are read from configuration files held in the WEB-INF directory.

The WEBINF/isis.properties file is always read and must exist.

In addition, the following other properties are searched for and if present also read:

  • viewer_wicket.properties - if the Wicket UI (viewer) is in use

  • viewer_restfulobjects.properties - if the Restful Objects REST API (viewer) is in use

  • viewer.properties - for any shared UI configuration

  • persistor_datanucleus.properties - assuming the JDO/DataNucleus objectstore is in use

  • persistor.properties - for any other objectstore configuration. This typically is used to hold JDBC URLs

  • authentication_shiro.properties, authorization_shiro.properties

    assuming the Shiro Security is in use (but there are no security-related config properties currently; use shiro.ini for Shiro config)

  • authentication.properties, authorization.properties

    for any other security-related config properties (but there are none currently).

You can if you wish simply store all properties in the isis.properties file; but we think that breaking properties out into sections is preferable.

9.3. Specifying components

The isis.properties file has four configuration properties in particular that specify the major components of Isis to use.

They are:

Table 4. Core Configuration Properties
Property Value
(default value)
Implements

isis.authentication

shiro, bypass, FQCN
(shiro)

o.a.i.core.runtime.authentication. AuthenticationManagerInstaller

isis.authorization

shiro, bypass, FQCN
(shiro)

o.a.i.core.runtime.authorization. AuthorizationManagerInstaller

isis.persistor

datanucleus, inmemory, FQCN
(datanucleus)

o.a.i.core.runtime.installerregistry.installerapi. PersistenceMechanismInstaller

isis.services-installer

configuration, configuration-and-annotation, FQCN
(configuration)

org.apache.isis.core.runtime.services. ServicesInstaller

The mechanism to discover and load domain services:

  • configuration-and-annotation will search for @DomainService-annotated classes and also read from isis.services configuration property

  • configuration will only read from the isis.services configuration property.

  • Otherwise an alternative implementation of the o.a.i.core.runtime.services.ServicesInstaller internal API can be provided.

The values "datanucleus", "shiro" etc are actually aliases for concrete implementations listed in Isis' installer-registry.properties file (in isis-core-runtime.jar).

It is — at least in theory — possible to specify a fully qualified class name to replace any of these components. This is probably feasible for the two security APIs and the services-installer API; but replacing the persistor (JDO/DataNucleus) is much trickier because we rely on the JDO/DN for certain functionality (such as object dirtying and lazy loading) that is not defined within this API.

As for the viewers, these are specified indirectly by way of the filters and servlets in the web.xml file (discussed below). However, the configuration of which viewers to initialize is declared through a context parameter:

<context-param>
    <param-name>isis.viewers</param-name>
    <param-value>wicket,restfulobjects</param-value>
</context-param>

The net effect of this configuration is simply to ensure that the viewer_wicket.properties and/or the viewer_restfulobjects.properties files are read.

9.4. Configuring Core

This section lists the core/runtime configuration properties recognized by Apache Isis.

Configuration properties for the JDO/DataNucleus objectstore can be found in the Configuring DataNucleus section later in this chapter, while configuration properties for the viewers can be found in the their respective chapters, here for Wicket viewer, and here for the Restful Objects viewer.

Table 5. Core Configuration Properties
Property Value
(default value)
Description

isis.object.
editing

true,false (true)

Whether objects' properties and collections can be edited directly (for objects annotated with @DomainObject#editing()); see below for further discussion.

isis.persistor.
disableConcurrencyChecking

true,false (false)

Disables concurrency checking globally.

Only intended for "emergency use" as a workaround while pending fix/patch to Isis itself. (Note that there is no "datanucleus" in the property).

isis.reflector.facet.
cssClass.patterns

regex:css1,regex2:css2,…​

Comma separated list of key:value pairs, where the key is a regex matching action names (eg delete.*) and the value is a Bootstrap CSS button class (eg btn-warning) to be applied (as per `@CssClass()) to all action members matching the regex.

See UI hints for more details.

isis.reflector.facet.
cssClassFa.patterns

regex:fa-icon,regex2:fa-icon2,…​

Comma separated list of key:value pairs, where the key is a regex matching action names (eg create.*) and the value is a font-awesome icon name (eg fa-plus) to be applied (as per @CssClassFa()) to all action members matching the regex.

See UI hints for more details.

isis.reflector.facets

FQCN

Fully qualified class names of a custom implementation of ProgrammingModel interface.

See finetuning the programming model for more details.

isis.reflector.facets.
exclude

FQCN,FQCN2,…​

Fully qualified class names of (existing, built-in) facet factory classes to be included to the programming model.

See finetuning the programming model for more details.

isis.reflector.facets.
include

FQCN,FQCN2,…​

Fully qualified class names of (new, custom) facet factory classes to be included to the programming model.
See finetuning the programming model for more details.

isis.reflector.
layoutMetadataReaders

FQCN,FQCN2,…​

Fully qualified class names of classes to be instantiated to read layout metadata, as used in for dynamic layouts.

See Layout Metadata Reader for more information.

isis.reflector.validator

FQCN

Custom implementation of MetaModelValidator (in the org.apache.isis.core.metamodel.specloader.validator package)

See Custom Validator to learn more.

isis.reflector.validator.
allowDeprecated

true,false (true)

Whether deprecated annotations or naming conventions are tolerated or not. If not, then a metamodel validation error will be triggered, meaning the app won’t boot (fail-fast).

isis.services

FQCN,FQCN2,…​

Fully qualified class names of classes to be instantiated as domain services.

Each entry can be optionally prefixed by "n:" specifying the relative order on the menu (corresponds to @DomainServiceLayout#menuOrder()).

isis.services.
audit.objects

all, none (all)

Whether the changed properties of objects should be automatically audited (for objects annotated with @DomainObject(auditing=Auditing.AS_CONFIGURED).

isis.services.
command.actions

all, ignoreSafe, none (all)

Whether actions should be automatically reified into commands (for actions annotated with @Action(command=CommandReification.AS_CONFIGURED).

ignoreQueryOnly is an alias for ignoreSafe.

isis.services.
container.disableAutoFlush

true,false (false)

Whether the DomainObjectContainer should automatically flush pending changes prior to querying (via allMatches(), firstMatch() and so on).

isis.service.
email.tls.enabled

true,false (true)

Whether to enable TLS for the email SMTP connection (used by EmailService).

NB: note that the key is mis-spelt, (isis.service.email rather than isis.services.email)

isis.service.
email.sender.hostname

host (smtp.gmail.com)

The hostname of the external SMTP provider (used by EmailService).

NB: note that the key is mis-spelt, (isis.service.email rather than isis.services.email)

isis.service.
email.port

port number (587)

The port number for the SMTP service on the the external SMTP host (used by EmailService).

NB: note that the key is mis-spelt, (isis.service.email rather than isis.services.email)

isis.service.
email.sender.address

email address

The email address to use for sending out email (used by EmailService). Mandatory.

NB: note that the key is mis-spelt, (isis.service.email rather than isis.services.email)

isis.service.
email.sender.password

email password

The corresponding password for the email address to use for sending out email (used by EmailService). Mandatory.

NB: note that the key is mis-spelt, (isis.service.email rather than isis.services.email)

isis.services.
eventbus.implementation

guava, axon, FQCN (guava)

which implementation to use by the EventBusService as the underlying event bus.

isis.services.
eventbus.allowLateRegistration

true, false, (false)

whether a domain service can register with the EventBusService after any events have posted.

Since this almost certainly constitutes a bug in application code, by default this is disallowed.

isis.services.
publish.objects

all, none (all)

Whether changed objects should be automatically published (for objects annotated with @DomainObject(publishing=Publishing.AS_CONFIGURED).

isis.services.
publish.actions

all, ignoreSafe, none (all)

Whether actions should be automatically published (for actions annotated with @Action(publishing=Publishing.AS_CONFIGURED).

isis.services.
translation.po.mode

read,write

Whether to force the TranslationService into either read or write mode.

See i18n support to learn more about the translation service.

isis.viewers.
paged.parented

positive integer (12)

Default page size for parented collections (as owned by an object, eg Customer#getOrders())

isis.viewers.
paged.standalone

positive integer (25)

Default page size for standalone collections (as returned from an action invocation)

isis.viewers.
propertyLayout.labelPosition

TOP, LEFT
(LEFT)

Default for label position for all properties if not explicitly specified using @PropertyLayout#labelPosition()

9.4.1. objects.editing

This configuration property in effect allows editing to be disabled globally for an application:

isis.objects.editing=false

We recommend enabling this feature; it will help drive out the underlying business operations (processes and procedures) that require objects to change; these can then be captured as business actions.

9.4.2. propertyLayout.labelPosition

If you want a consistent look-n-feel throughout the app, eg all property labels to the top, then it’d be rather frustrating to have to annotate every property.

Instead, a default can be specified in isis.properties:

isis.viewers.propertyLayout.labelPosition=TOP

or

isis.viewers.propertyLayout.labelPosition=LEFT

If these are not present then Isis will render according to internal defaults. At the time of writing, this means labels are to the left for all datatypes except multiline strings.

9.5. Configuring DataNucleus

Apache Isis programmatically configures DataNucleus; any Isis properties with the prefix isis.persistor.datanucleus.impl are passed through directly to the JDO/DataNucleus objectstore (with the prefix stripped off, of course).

DataNucleus will for itself also and read the META-INF/persistence.xml; at a minimum this defines the name of the "persistence unit". n theory it could also hold mappings, though in Isis we tend to use annotations instead.

Furthermore, DataNucleus will search for various other XML mapping files, eg mappings.jdo. A full list can be found here. The metadata in these XML can be used to override the annotations of annotated entities; see Overriding JDO Annotatons for further discussion.

9.5.1. Configuration Properties

These configuration properties are typically stored in WEB-INF/persistor_datanucleus.properties. However, you can place all configuration properties into WEB-INF/isis.properties if you wish (the configuration properties from all config files are merged together).

Configuration Properties for Apache Isis itself
Table 6. JDO/DataNucleus Objectstore Configuration Properties
Property Value
(default value)
Description

isis.persistor.
datanucleus.
classMetadataLoadedListener

FQCN

The default (o.a.i.os.jdo.dn.CreateSchemaObjectFromClassMetadata) creates a DB schema object

isis.persistor.datanucleus.
RegisterEntities.packagePrefix

fully qualified package names (CSV)

of class names; specifies the entities early rather than allow DataNucleus to find the entities lazily. Strongly recommended (subtle issues can sometimes arise if lazy discovery is used). Further discussion below.

isis.persistor.datanucleus.
PublishingService.serializedForm

zipped

Configuration Properties passed through directly to DataNucleus.
Table 7. JDO/DataNucleus Objectstore Configuration Properties
Property Value
(default value)
Description

isis.persistor.datanucleus.impl.*

Passed through directly to Datanucleus (with isis.persistor.datanucleus.impl prefix stripped)

isis.persistor.datanucleus.impl.
datanucleus.persistenceByReachabilityAtCommit

false

We recommend this setting is disabled.
Further discussion below.

9.5.2. persistence.xml

TODO

9.5.3. Eagerly Registering Entities

Both Apache Isis and DataNucleus have their own metamodels of the domain entities. Isis builds its metamodel by walking the graph of types from the services registered using @DomainService or explicitly registered in isis.properties. The JDO objectstore then takes these types and registers them with DataNucleus.

In some cases, though, not every entity type is discoverable from the API of the service actions. This is especially the case if you have lots of subtypes (where the action method specifies only the supertype). In such cases the Isis and JDO metamodels is built lazily, when an instance of that (sub)type is first encountered.

Isis is quite happy for the metamodel to be lazily created, and - to be fair - DataNucleus also works well in most cases. In some cases, though, we have found that the JDBC driver (eg HSQLDB) will deadlock if DataNucleus tries to submit some DDL (for a lazily discovered type) intermingled with DML (for updating).

In any case, it’s probably not good practice to have DataNucleus work this way. The RegisterEntities service can therefore be registered in order to do the eager registration. It searches for all @PersistenceCapable entities under specified package(s), and registers them all.

There’s a chance that (from 1.6.0+) feature may be (partly?) broken; see ISIS-847.

Specify the Package Prefix(es)

In the persistor_datanucleus.properties, specify the package prefix(es) of your application, to provide a hint for finding the @PersistenceCapable classes.

The value of this property can be a comma-separated list (if there is more than one package or Maven module that holds persistable entities).

Integration Testing

The IsisConfigurationForJdoIntegTests, recommended for use in Integration Testing provides the #addRegisterEntitiesPackagePrefix() method to set up this configuration property:

Integration test bootstrapping
private static class SimpleAppSystemBuilder extends IsisSystemForTest.Builder {
    ...
    private static IsisConfiguration testConfiguration() {
        final IsisConfigurationForJdoIntegTests testConfiguration = new IsisConfigurationForJdoIntegTests();
        testConfiguration.addRegisterEntitiesPackagePrefix("domainapp.dom.modules"); (1)
        return testConfiguration;
    }
}
1 specify the package prefix(es) for integration testing.

9.5.4. Persistence by Reachability

By default, JDO/DataNucleus supports the concept of persistence-by-reachability. That is, if a non-persistent entity is associated with an already-persistent entity, then DataNucleus will detect this and will automatically persist the associated object. Put another way: there is no need to call Isis' DomainObjectContainer#persist(.) or DomainObjectContainer#persistIfNotAlready(.) methods.

However, convenient though this feature is, you may find that it causes performance issues.

DataNucleus' persistence-by-reachability may cause performance issues. We strongly recommend that you disable it.

One scenario in particular where this performance issues can arise is if your entities implement the java.lang.Comparable interface, and you have used Isis' ObjectContracts utility class. The issue here is that ObjectContracts implementation can cause DataNucleus to recursively rehydrate a larger number of associated entities. (More detail below).

We therefore recommend that you disable persistence-by-reachability by adding the following to persistor_datanucleus.properties:

isis.persistor.datanucleus.impl.datanucleus.persistenceByReachabilityAtCommit=false

This change has been made to the SimpleApp archetype

If you do disable this feature, then you will (of course) need to ensure that you explicitly persist all entities using the DomainObjectContainer#persist(.) or DomainObjectContainer#persistIfNotAlready(.) methods.

The issue in more detail

Consider these entities (yuml.me/b8681268):

party agreementrole agreement

In the course of a transaction, the Agreement entity is loaded into memory (not necessarily modified), and then new AgreementRoles are associated to it.

All these entities implement Comparable using ObjectContracts, and the implementation of AgreementRole's (simplified) is:

public class AgreementRole {
    ...
    public int compareTo(AgreementRole other) {
        return ObjectContracts.compareTo(this, other, "agreement","startDate","party");
    }
}

while Agreement's is implemented as:

public class Agreement {
    ...
    public int compareTo(Agreement other) {
        return ObjectContracts.compareTo(this, other, "reference");
    }
}

and Party's is similarly implemented as:

public class Party {
    ...
    public int compareTo(Party other) {
        return ObjectContracts.compareTo(this, other, "reference");
    }
}

DataNucleus’s persistence-by-reachability algorithm adds the AgreementRole instances into a SortedSet, which causes AgreementRole#compareTo() to fire:

  • the evaluation of the "agreement" property delegates back to the Agreement, whose own Agreement#compareTo() uses the scalar reference property. As the Agreement is already in-memory, this does not trigger any further database queries

  • the evaluation of the "startDate" property is just a scalar property of the AgreementRole, so will already in-memory

  • the evaluation of the "party" property delegates back to the Party, whose own Party#compareTo() requires the uses the scalar reference property. However, since the Party is not yet in-memory, using the reference property triggers a database query to "rehydrate" the Party instance.

In other words, in figuring out whether AgreementRole requires the persistence-by-reachability algorithm to run, it causes the adjacent associated entity Party to also be retrieved.

9.5.5. Using JNDI DataSource

Isis' JDO objectstore can be configured either to connect to the database using its own connection pool, or by using a container-managed datasource.

Application managed

Using a connection pool managed directly by the application (that is, by Isis' JDO objectstore and ultimately by DataNucleus) requires a single set of configuration properties to be specified.

In the WEB-INF\persistor_datanucleus.properties file, specify the connection driver, url, username and password.

For example:

isis.persistor.datanucleus.impl.javax.jdo.option.ConnectionDriverName=net.sf.log4jdbc.DriverSpy
isis.persistor.datanucleus.impl.javax.jdo.option.ConnectionURL=jdbc:log4jdbc:hsqldb:mem:test
isis.persistor.datanucleus.impl.javax.jdo.option.ConnectionUserName=sa
isis.persistor.datanucleus.impl.javax.jdo.option.ConnectionPassword=

Those configuration properties that start with the prefix isis.persistor.datanucleus.impl. are passed through directly to DataNucleus (with the prefix removed).

Container managed (JNDI)

Using a datasource managed by the servlet container requires three separate bits of configuration.

Firstly, specify the name of the datasource in the WEB-INF\persistor_datanucleus.properties file. For example:

If connection pool settings are also present in this file, they will simply be ignored. Any other configuration properties that start with the prefix isis.persistor.datanucleus.impl. are passed through directly to DataNucleus (with the prefix removed).

Secondly, in the WEB-INF/web.xml, declare the resource reference:

<resource-ref>
    <description>db</description>
    <res-ref-name>jdbc/simpleapp</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
    <res-auth>Container</res-auth>
</resource-ref>

Finally, declare the datasource as required by the servlet container. For example, if using Tomcat 7, the datasource can be specified by adding the following to $TOMCAT_HOME/conf/context.xml:

<Resource name="jdbc/simpleapp"
  auth="Container"
  type="javax.sql.DataSource"
  maxActive="100"
  maxIdle="30"
  maxWait="10000"
  username="sa"
  password="p4ssword"
  driverClassName="com.microsoft.sqlserver.jdbc.SQLServerDriver"
  url="jdbc:sqlserver://127.0.0.1:1433;instance=.;databaseName=simpleapp"/>

You will also need to make sure that the JDBC driver is on the servlet container’s classpath. For Tomcat, this means copying the driver to $TOMCAT_HOME/lib.

According to Tomcat’s documentation, it is supposedly possible to copy the conf/context.xml to the name of the webapp, eg conf/mywebapp.xml, and scope the connection to that webapp only. I was unable to get this working, however.

9.6. web.xml

TODO

9.6.1. Init Params

<isis.viewers>

9.6.2. Filters

TODO

9.6.3. Servlets

TODO

9.6.4. Running RO Viewer only

TODO

9.7. Application-specific CSS and JS

TODO

9.7.1. application.css

TODO - WEB-INF/css/application.css

9.7.2. application.js

TODO - WEB-INF/scripts/application.js

10. Isis Maven Plugin

TODO

11. Deployment

This chapter provides guidance on some common deployment scenarios.

11.1. Command Line

TODO

11.1.1. WebServer class

org.apache.isis.WebServer

For example: * --fixture * --services

etc…​

11.1.2. Dummy class

org.apache.isis.Dummy

etc…​

11.2. Deploying to Tomcat

TODO

11.2.1. Externalized Configuration

TODO

11.2.2. JVM Args

TODO

for the WrapperFactory (uses Javassist)

using:

-XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC

You can hint the JVM to unload dynamically created classes which ClassLoaders are still around.

11.2.3. Using a JNDI Datasource

See the guidance in the configuring datanucleus section.

11.3. Externalized Configuration

TODO

11.4. Deploying to Google App Engine

TODO

The Google App Engine (GAE) provides a JDO API, meaning that you can deploy Isis onto GAE using the JDO objectstore.

However, GAE is not an RDBMS, and so there are some limitations that it imposes. This page gathers together various hints, tips and workarounds.

11.4.1. Primary Keys and Owned/Unowned Relationships

All entities must have a @PrimaryKey. Within GAE, the type of this key matters.

For an entity to be an aggregate root, (ie a root of an GAE entity group), its key must be a Long, eg:

Any collection that holds this entity type (eg ToDoItem#dependencies holding a collection of ToDoItem`s) should then be annotated with `@Unowned (a GAE annotation).

If on the other hand you want the object to be owned (through a 1:m relationship somewhere) by some other root, then use a String:

Note: if you store a relationship with a String key it means that the parent object owns the child, any attempt to change the relationship raise and exception.

11.4.2. Custom Types

Currently Isis' Blob and Clob types and the JODA types (LocalDate et al) are not supported in GAE.

Instead, GAE defines a fixed set of value types, including BlobKey. Members of the Isis community have this working, though I haven’t seen the code.

The above notwithstanding, Andy Jefferson at DataNucleus tells us:

GAE JDO/JPA does support some type conversion, because looking at StoreFieldManager.java for any field that is Object-based and not a relation nor Serialized it will call TypeConverstionUtils.java and that looks for a TypeConverter (specify @Extension with key of "type-converter-name" against a field and value as the TypeConverter class) and it should convert it. Similarly when getting the value from the datastore.

On further investigation, it seems that the GAE implementation performs a type check on a SUPPORTED_TYPES Java set, in com.google.appengine.api.datastore.DataTypeUtils:

if (!supportedTypes.contains(value.getClass())) {
     throw new IllegalArgumentException(prefix + value.getClass().getName() + " is not a supported property type.");
}

We still need to try out Andy’s recipe, above.

11.5. Neo4J

As of 1.8.0 Apache Isis has experimental support for Neo4J, courtesy of DataNucleus' Neo4J Datastore implementation.

The SimpleApp archetype has been updated so that they can be optionally run under Neo4J.

In addition, the Isis addons' neoapp (non-ASF) is configured to run with an embedded Neo4J server running alongside the Isis webapp.

The steps below describe the configuration steps required to update an existing app.

11.5.1. ConnectionURL

In persistor.properties, update the JDO ConnectionURL property, eg:

isis.persistor.datanucleus.impl.javax.jdo.option.ConnectionURL=neo4j:neo4j_DB

The other connection properties (ConnectionDriverName, ConnectionUserName and ConnectionPassword) should be commented out.

11.5.2. Update pom.xml

Add the following dependency to the webapp project’s pom.xml:

<dependencies>
    ...
    <dependency>
        <groupId>org.datanucleus</groupId>
        <artifactId>datanucleus-neo4j</artifactId>
        <version>3.2.3</version> (1)
    </dependency>
    ...
</dependencies>
1 for 1.8.0, use the value shown. for 1.9.0-SNAPSHOT, use 4.0.5.

In the SimpleApp archetype this is defined under the "neo4j" profile so can be activated using -P neo4j.

11.5.3. Try it out!

If you want to quickly try out neo4j for yourself:

If you visit the about page you should see the neo4j JAR files are linked in, and a neo4j_DB subdirectory within the webapp directory.

11.6. JVM Flags

TODO

12. Testing

If you are going to use Apache Isis for developing complex business-critical applications, then being able to write automated tests for those applications becomes massively important. As such Apache Isis treats the topic of testing very seriously, and (though we say it ourselves) has support that goes way above what is provided by other application frameworks.

12.1. Overview

12.1.1. Unit tests vs Integ tests

We divide automated tests into two broad categories:

  • unit tests exercise a single unit (usually a method) of a domain object, in isolation.

    Dependencies of that object are mocked out. These are written by a developer and for a developer; they are to ensure that a particular "cog in the machine" works correctly

  • integration tests exercise the application as a whole, usually focusing on one particular business operation (action).

    These are tests that represent the acceptance criteria of some business story; their intent should make sense to the domain expert (even if the domain expert is "non-technical")

To put it another way:

Integration tests help ensure that you are building the right system

Unit tests help ensure that you are building the system right.

12.1.2. Integ tests vs BDD Specs

We further sub-divide integration tests into:

  • those that are implemented in Java and JUnit (we call these simply "integration tests")

    Even if a domain expert understands the intent of these tests, the actual implementation will be opaque to them. Also, the only output from the tests is a (hopefully) green CI job

  • tests (or rather, specifications) that are implemented in a behaviour-driven design (BDD) language such as Cucumber (we call these "BDD specs")

    The natural language specification then maps down onto some glue code that is used to drive the application. But the benefits of taking a BDD approach include the fact that your domain expert will be able to read the tests/specifications, and that when you run the specs, you also get documentation of the application’s behaviour ("living documentation").

It’s up to you whether you use BDD specs for your apps; it will depend on your development process and company culture. But you if don’t then you certainly should write integration tests: acceptance criteria for user stories should be automated!

12.1.3. Simulated UI (WrapperFactory)

When we talk about integration tests/specs here, we mean tests that exercise the domain object logic, through to the actual database. But we also want the tests to exercise the app from the users’s perspective, which means including the user interface.

For most other frameworks that would require having to test the application in a very heavy weight/fragile fashion using a tool such as Selenium, driving a web browser to navigate . In this regard though, Isis has a significant trick up its sleeve. Because Isis implements the naked objects pattern, it means that the UI is generated automatically from the UI. This therefore allows for other implementations of the UI.

The WrapperFactory domain service allows a test to wrap domain objects and thus to interact with said objects "as if" through the UI:

integ tests

If the test invokes an action that is disabled, then the wrapper will throw an appropriate exception. If the action is ok to invoke, it delegates through.

What this means is that an Isis application can be tested end-to-end without having to deploy it onto a webserver; the whole app can be tested while running in-memory. Although integration tests re (necessarily) slower than unit tests, they are not any harder to write (in fact, in some respects they are easier).

12.1.4. Dependency Injection

Isis provides autowiring dependency injection into every domain object. This is most useful when writing unit tests; simply mock out the service and inject into the domain object.

There are a number of syntaxes supported, but the simplest is to use @javax.inject.Inject annotation; for example:

@javax.inject.Inject
CustomerRepository customers;

Isis can inject into this even if the field has package-level (or even private) visibility. We recommend that you use package-level visibility, though, so that your unit tests (in the same package as the class under test) are able to inject mocks.

Isis does also support a couple of other syntaxes:

public void setCustomerRepository(CustomerRepository customers) { ... }

or

public void injectCustomerRepository(CustomerRepository customers) { ... }

Apache Isis also supports automatic dependency injection into integration tests; just declare the service dependency in the usual fashion and it will be automatically injected.

12.1.5. Given/When/Then

Whatever type of test/spec you are writing, we recommend you follow the given/when/then idiom:

  • given the system is in this state (preconditions)

  • when I poke it with a stick

  • then it looks like this (postconditions)

A good test should be 5 to 10 lines long; the test should be there to help you reason about the behaviour of the system. Certainly if the test becomes more than 20 lines it’ll be too difficult to understand.

The "when" part is usually a one-liner, and in the "then" part there will often be only two or three assertions that you want to make that the system has changed as it should.

For unit test the "given" part shouldn’t be too difficult either: just instantiate the class under test, wire in the appropriate mocks and set up the expectations. And if there are too many mock expectations to set up, then "listen to the tests" …​ they are telling you your design needs some work.

Where things get difficult though is the "given" for integration tests; which is the topic of the next section…​

12.1.6. Fixture Management

In the previous section we discussed using given/when/then as a form of organizing tests, and why you should keep your tests small.

For integration tests though it can be difficult to keep the "given" short; there could be a lot of prerequisite data that needs to exist before you can actually exercise your system. Moreover, however we do set up that data, but we also want to do so in a way that is resilient to the system changing over time.

The solution that Isis provides is a domain service called Fixture Scripts, that defines a pattern and supporting classes to help ensure that the "data setup" for your tests are reusable and maintainable over time.

12.1.7. Fake data

In any given test there are often quite a few variables involved, to initialize the state of the objects, or to act as arguments for invoking a method, or when asserting on post-conditions. Sometimes those values are important (eg verifying that an `Order’s state went from PENDING to SHIPPED, say), but often they aren’t (a customer’s name, for example) but nevertheless need to be set up (especially in integration tests).

We want our tests to be easily understood, and we want the reader’s eye to be drawn to the values that are significant and ignore those that are not.

One way to do this is to use random (or fake) values for any insignificant data. This in effect tells the reader that "any value will do". Moreover, if it turns out that any data won’t do, and that there’s some behaviour that is sensitive to the value, then the test will start to flicker, passing and then failing depending on inputs. This is A Good Thing™.

Apache Isis does not, itself, ship with a fake data library. However, the Isis addons' fakedata module (non-ASF) does provide exactly this capability.

Using fake data works very well with fixture scripts; the fixture script can invoke the business action with sensible (fake/random) defaults, and only require that the essential information is passed into it by the test.

12.1.8. Feature Toggles

Writing automated tests is just good development practice. Also good practice is developing on the mainline (master, trunk); so that your continuous integration system really is integrating all code. Said another way: don’t use branches!

Sometimes, though, a feature will take longer to implement than your iteration cycle. In such a case, how do you use continuous integration to keep everyone working on the mainline without revealing a half-implemented feature on your releases? One option is to use feature toggles.

Apache Isis does not, itself, ship with a feature toggle library. However, the Isis addons' togglz module (non-ASF) does provide exactly this capability.

With all that said, let’s look in detail at the testing features provided by Apache Isis.

12.2. Unit Test Support

Isis Core provides a number of unit test helpers for you to use (if you wish) to unit test your domain objects.

12.2.1. Contract Tests

Contract tests ensure that all instances of a particular idiom/pattern that occur within your codebase are implemented correctly. You could think of them as being a way to enforce a certain type of coding standard. Implementation-wise they use Reflections library to scan for classes.

SortedSets Contract Test

This contract test automatically checks that all fields of type java.util.Collection are declared as java.util.SortedSet. In other words, it precludes either java.util.List or java.util.Set from being used as a collection.

For example, the following passes the contract test:

public class Department {
    private SortedSet<Employee> employees = new TreeSet<Employee>();
    ...
}

whereas this would not:

public class SomeDomainObject {
    private List<Employee> employees = new ArrayList<Employee>();
    ...
}

If using DataNucleus against an RDBMS (as you probably are) then we strongly recommend that you implement this test, for several reasons:

  • first, Sets align more closely to the relational model than do List`s. A `List must have an additional index to specify order.

  • second, SortedSet is preferable to Set because then the order is well-defined and predictable (to an end user, to the programmer).

    The [ObjectContracts] utility class substantially simplifies the task of implementing Comparable in your domain classes.

  • third, if the relationship is bidirectional then JDO/Objectstore will automatically maintain the relationship.

To use the contract test, subclass SortedSetsContractTestAbstract, specifying the root package to search for domain classes.

For example:

public class SortedSetsContractTestAll extends SortedSetsContractTestAbstract {

    public SortedSetsContractTestAll() {
        super("org.estatio.dom");
        withLoggingTo(System.out);
    }
}
Bidirectional Contract Test

This contract test automatically checks that bidirectional 1:m or 1:1 associations are being maintained correctly (assuming that they follow the mutual registration pattern

(If using the JDO objectstore, then) there is generally no need to programmatically maintain 1:m relationships (indeed it may introduce subtle errors). For more details, see here. Also check out the IDE templates for further guidance.

For example, suppose that ParentDomainObject and ChildDomainObject have a 1:m relationship (ParentDomainObject#children / ChildDomainObject#parent), and also PeerDomainObject has a 1:1 relationship with itself (PeerDomainObject#next / PeerDomainObject#previous).

The following will exercise these relationships:

public class BidirectionalRelationshipContractTestAll
        extends BidirectionalRelationshipContractTestAbstract {

    public BidirectionalRelationshipContractTestAll() {
        super("org.apache.isis.core.unittestsupport.bidir",
                ImmutableMap.<Class<?>,Instantiator>of(
                    ChildDomainObject.class, new InstantiatorForChildDomainObject(),
                    PeerDomainObject.class, new InstantiatorSimple(PeerDomainObjectForTesting.class)
                ));
        withLoggingTo(System.out);
    }
}

The first argument to the constructor scopes the search for domain objects; usually this would be something like "com.mycompany.dom".

The second argument provides a map of Instantiator for certain of the domain object types. This has two main purposes. First, for abstract classes, it nominates an alternative concrete class to be instantiated. Second, for classes (such as ChildDomainObject) that are Comparable and are held in a SortedSet), it provides the ability to ensure that different instances are unique when compared against each other. If no Instantiator is provided, then the contract test simply attempts to instantiates the class.

If any of the supporting methods (addToXxx(), removeFromXxx(), modifyXxx() or clearXxx()) are missing, the relationship is skipped.

To see what’s going on (and to identify any skipped relationships), use the withLoggingTo() method call. If any assertion fails then the error should be descriptive enough to figure out the problem (without enabling logging).

The example tests can be found here.

Injected Services Method Contract Test

It is quite common for some basic services to be injected in a project-specific domain object superclass; for example a ClockService might generally be injected everywhere:

public abstract class EstatioDomainObject {
    @javax.inject.Inject
    protected ClockService clockService;
}

If a subclass inadvertantly overrides this method and provides its own ClockService field, then the field in the superclass will never initialized. As you might imagine, NullPointerExceptions could then arise.

This contract test automatically checks that the injectXxx(…​) method, to allow for injected services, is not overridable, ie final.

This contract test is semi-obsolete; most of the time you will want to use @javax.inject.Inject on fields rather than the injectXxx() method. The feature dates from a time before Isis supported the @Inject annotation.

To use the contract test, , subclass SortedSetsContractTestAbstract, specifying the root package to search for domain classes.

For example:

public class InjectServiceMethodMustBeFinalContractTestAll extends InjectServiceMethodMustBeFinalContractTestAbstract {

    public InjectServiceMethodMustBeFinalContractTestAll() {
        super("org.estatio.dom");
        withLoggingTo(System.out);
    }
}
Value Objects Contract Test

The ValueTypeContractTestAbstract automatically tests that a custom value type implements equals() and hashCode() correctly.

For example, testing JDK’s own java.math.BigInteger can be done as follows:

public class ValueTypeContractTestAbstract_BigIntegerTest extends ValueTypeContractTestAbstract<BigInteger> {

    @Override
    protected List<BigInteger> getObjectsWithSameValue() {
        return Arrays.asList(new BigInteger("1"), new BigInteger("1"));
    }

    @Override
    protected List<BigInteger> getObjectsWithDifferentValue() {
        return Arrays.asList(new BigInteger("2"));
    }
}

The example unit tests can be found here.

12.2.2. JMock Extensions

As noted earlier, for unit tests we tend to use JMock as our mocking library. The usual given/when/then format gets an extra step:

  • given the system is in this state

  • expecting these interactions (set up the mock expectations here)

  • when I poke it with a stick

  • then these state changes and interactions with Mocks should have occurred.

If using JMock then the interactions (in the "then") are checked automatically by a JUnit rule. However, you probably will still have some state changes to assert upon.

Distinguish between queries vs mutators

For mock interactions that simply retrieve some data, your test should not need to verify that it occurred. If the system were to be refactored and starts caching some data, you don’t really want your tests to start breaking because they are no longer performing a query that once they did. If using JMock API this means using the allowing(..) method to set up the expectation.

On the other hand mocks that mutate the state of the system you probably should assert have occurred. If using JMock this typically means using the oneOf(…​) method.

For more tips on using JMock and mocking in general, check out the GOOS book, written by JMock’s authors, Steve Freeman and Nat Pryce and also Nat’s blog.

Apache Isis' unit test support provides JUnitRuleMockery2 which is an extension to the JMock's JunitRuleMockery. It provides a simpler API and also providing support for autowiring.

For example, here we see that the class under test, an instance of CollaboratingUsingSetterInjection, is automatically wired up with its Collaborator:

public class JUnitRuleMockery2Test_autoWiring_setterInjection_happyCase {

    @Rule
    public JUnitRuleMockery2 context = JUnitRuleMockery2.createFor(Mode.INTERFACES_AND_CLASSES);

    @Mock
    private Collaborator collaborator;

    @ClassUnderTest
    private CollaboratingUsingSetterInjection collaborating;

    @Test
    public void wiring() {
        assertThat(collaborating.collaborator, is(not(nullValue())));
    }
}

Isis also includes (and automatically uses) a Javassist-based implementation of JMock’s ClassImposteriser interface, so that you can mock out concrete classes as well as interfaces. We’ve provided this rather than JMock’s own cglib-based implementation (which is problematic for us given its own dependencies on asm).

The example tests can be found here

12.2.3. Maven Configuration

Apache Isis' unit test support is automatically configured if you use the SimpleApp archetype. To set it up manually, update the pom.xml of your domain object model module:

<dependency>
    <groupId>org.apache.isis.core</groupId>
    <artifactId>isis-core-unittestsupport</artifactId>
    <scope>test</scope> (1)
</dependency>
1 Normally test; usual Maven scoping rules apply.

We also recommend that you configure the maven-surefire-plugin to pick up the following class patterns:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.10</version>
    <configuration>
        <includes>
            <include>**/*Test.java</include>
            <include>**/*Test_*.java</include>
            <include>**/*Spec*.java</include>
        </includes>
        <excludes>
            <exclude>**/Test*.java</exclude>
            <exclude>**/*ForTesting.java</exclude>
            <exclude>**/*Abstract*.java</exclude>
        </excludes>
        <useFile>true</useFile>
        <printSummary>true</printSummary>
        <outputDirectory>${project.build.directory}/surefire-reports</outputDirectory>
    </configuration>
</plugin>

12.3. Integration Test Support

As discussed in the introductory overview of this chapter, Apache Isis allows you to integration test your domain objects from within JUnit. There are several parts to this:

  • configuring the Isis runtime so it can be bootstrapped (mostly boilerplate)

  • defining a base class to perform said bootstrapping

  • using fixture scripts to set up the app

  • using WrapperFactory so that the UI can be simulated.

We’ll get to all that shortly, but let’s start by taking a look at what a typical integration test looks like.

12.3.1. Typical Usage

This example adapted from the Isis addons' todoapp (non-ASF). The code we want to test (simplified) is:

public class ToDoItem ... {

    private boolean complete;
    @Property( editing = Editing.DISABLED )
    public boolean isComplete() {
        return complete;
    }
    public void setComplete(final boolean complete) {
        this.complete = complete;
    }

    @Action()
    public ToDoItem completed() {
        setComplete(true);
        ...
        return this;
    }
    public String disableCompleted() {
        return isComplete() ? "Already completed" : null;
    }
    ...
}

We typically put the bootstrapping of Isis into a superclass (AbstractToDoIntegTest below), then subclass as required.

For this test (of the "completed()" action) we need an instance of a ToDoItem that is not yet complete. Here’s the setup:

public class ToDoItemIntegTest extends AbstractToDoIntegTest {

    @Inject
    FixtureScripts fixtureScripts;                              (1)
    @Inject
    ToDoItems toDoItems;                                        (2)
    ToDoItem toDoItem;                                          (3)

    @Before
    public void setUp() throws Exception {
        RecreateToDoItemsForCurrentUser fixtureScript =         (4)
            new RecreateToDoItemsForCurrentUser();
        fixtureScripts.runFixtureScript(fixtureScript, null);
        final List<ToDoItem> all = toDoItems.notYetComplete();  (5)
        toDoItem = wrap(all.get(0));                            (6)
    }
    ...
}
1 the FixtureScripts domain service is injected, providing us with the ability to run fixture scripts
2 likewise, an instance of the ToDoItems domain service is injected. We’ll use this to lookup…​
3 the object under test, held as a field
4 the fixture script for this test; it deletes all existing todo items (for the current user only) and then recreates them
5 we lookup one of the just-created todo items…​
6 and then wrap it so that our interactions with it are as if through the UI

The following code tests the happy case, that a not-yet-completed ToDoItem can be completed by invoking the completed() action:

public class ToDoItemIntegTest ... {
    ...
    public static class Completed extends ToDoItemIntegTest { (1)
        @Test
        public void happyCase() throws Exception {
            // given
            assertThat(toDoItem.isComplete()).isFalse();      (2)
            // when
            toDoItem.completed();
            // then
            assertThat(toDoItem.isComplete()).isTrue();
        }
        ...
    }
}
1 the idiom we follow is to use nested static classes to identify the class responsibility being tested
2 the todoapp uses AssertJ.

What about when a todo item is already completed? The disableCompleted() method in the class says that it shouldn’t be allowed (the action would be greyed out in the UI with an appropriate tooltip). The following test verifies this:

        @Test
        public void cannotCompleteIfAlreadyCompleted() throws Exception {
            // given
            unwrap(toDoItem).setComplete(true);                     (1)
            // expect
            expectedExceptions.expectMessage("Already completed");  (2)
            // when
            toDoItem.completed();
        }
1 we unwrap the domain object in order to set its state directly
2 the expectedExceptions JUnit rule (defined by a superclass) verifies that the appropiate exception is indeed thrown (in the "when")

And what about the fact that the underlying "complete" property is disabled (the @Disabled annotation?). If the ToDoItem is put into edit mode in the UI, the complete checkbox should remain read-only. Here’s a verify similar test that verifies this also:

        @Test
        public void cannotSetPropertyDirectly() throws Exception {
            // expect
            expectedExceptions.expectMessage("Always disabled");  (1)
            // when
            toDoItem.setComplete(true);
        }
1 again we use the exceptedExceptions rule.

12.3.2. Bootstrapping

Integration tests instantiate an Isis "runtime" (as a singleton) within a JUnit test. Because (depending on the size of your app) it takes a little time to bootstrap Isis, the framework caches the runtime on a thread-local from one test to the next.

Nevertheless, we do need to bootstrap the runtime for the very first test.

Since the integration tests don’t depend on the webapp module, it’s necessary to repeat all the (relevant) all the configuration in isis.properties and related files. That said, in practice the relevant information isn’t all that much.

We hope to simplify the way this works in future versions of Isis.

The example code in this section is taken from the app generated by the SimpleApp archetype.

Builder

The bootstrapping itself is mostly performed by a subclass of the IsisSystemForTest.Builder class:

private static class SimpleAppSystemBuilder extends IsisSystemForTest.Builder {      (1)

    public SimpleAppSystemBuilder() {
        withLoggingAt(org.apache.log4j.Level.INFO);
        with(testConfiguration());
        with(new DataNucleusPersistenceMechanismInstaller());                        (2)

        withServicesIn( "domainapp" );                                               (3)
    }

    private static IsisConfiguration testConfiguration() {
        final IsisConfigurationForJdoIntegTests testConfiguration =
            new IsisConfigurationForJdoIntegTests();                                 (4)

        testConfiguration.addRegisterEntitiesPackagePrefix("domainapp.dom.modules"); (5)
        return testConfiguration;
    }
}
1 subclass the framework-provided IsisSystemForTest.Builder.
2 equivalent to isis.persistor=datanucleus in isis.properties
3 specify the isis.services key in isis.properties (where "domainapp" is the base package for all classes within the app)
4 IsisConfigurationForJdoIntegTests has pre-canned configuration for using an in-memory HSQLDB and other standard settings; more on this below.
5 equivalent to isis.persistor.datanucleus.RegisterEntities.packagePrefix key (typically in persistor_datanucleus.properties
IsisConfigurationForJdoIntegTests

Integration tests are configured programmatically, with a default set of properties to bootstrap the JDO/DataNucleus objectstore using an HSQLDB in-memory database.

To remove a little bit of boilerplate, the IsisConfigurationForJdoIntegTests class (in the org.apache.isis.objectstore.jdo.datanucleus package) can be used to bootstrap the application. If necessary, this class can be subclassed to override these defaults.

Table 8. Default Configuration Properties for Integration Tests
Property Value Description

isis.persistor.datanucleus.impl.
javax.jdo.option.ConnectionURL

jdbc:hsqldb:mem:test

JDBC URL

isis.persistor.datanucleus.impl.
javax.jdo.option.ConnectionDriverName

org.hsqldb.jdbcDriver

JDBC Driver

isis.persistor.datanucleus.impl.
javax.jdo.option.ConnectionUserName

sa

Username

isis.persistor.datanucleus.impl.
javax.jdo.option.ConnectionPassword

<empty string>

Password

isis.persistor.datanucleus.impl.
datanucleus.schema.autoCreateAll

true

Recreate DB for each test run (an in-memory database)

isis.persistor.datanucleus.impl.
datanucleus.schema.validateAll

false

Disable validations (minimize bootstrap time)

isis.persistor.datanucleus.impl.
datanucleus.persistenceByReachabilityAtCommit

false

As per WEB-INF/persistor_datanucleus.properties

isis.persistor.datanucleus.impl.
datanucleus.identifier.case

MixedCase

As per WEB-INF/persistor_datanucleus.properties

isis.persistor.datanucleus.impl.
datanucleus.cache.level2.type

none

As per WEB-INF/persistor_datanucleus.properties

isis.persistor.datanucleus.impl.
datanucleus.cache.level2.mode

ENABLE_SELECTIVE

As per WEB-INF/persistor_datanucleus.properties

isis.persistor.datanucleus.
install-fixtures

true

Automatically install any fixtures that might have been registered

isis.persistor.
enforceSafeSemantics

false

isis.deploymentType

server_prototype

System Initializer

The builder is defined and used within a class we call the system initializer that is responsible for instantiating the Isis runtime, and binding it to a thread-local. This is performed within a static initIsft() method.

Here’s the code:

public class SimpleAppSystemInitializer {
    public static void initIsft() {
        IsisSystemForTest isft = IsisSystemForTest.getElseNull();
        if(isft == null) {
            isft = new SimpleAppSystemBuilder()    (1)
                            .build()
                            .setUpSystem();
            IsisSystemForTest.set(isft);           (2)
        }
    }
    private static class SimpleAppSystemBuilder
        extends IsisSystemForTest.Builder { ... }

}
1 instantiates and initializes the Isis runtime (the IsisSystemForTest class)
2 binds the runtime to a thread-local.
Abstract Class

We recommend defining a base class for all your other classes to integration classes to inherit from. The main responsibility of this class is tocall the system initializer, described earlier. We only need the initialization to be performed once, so this call is performed in a @BeforeClass hook.

The code below shows the general form:

public abstract class SimpleAppIntegTest {
    @BeforeClass
    public static void initClass() {
        org.apache.log4j.PropertyConfigurator.configure("logging.properties");   (1)
        SimpleAppSystemInitializer.initIsft();                                   (2)
        new ScenarioExecutionForIntegration();                                   (3)
    }
}
1 ensure that logging messages don’t get swallowed
2 initialize the Isis runtime
3 primarily exists to support the writing of BDD specifications, but also enables finer-grained management of sessions/transactions (discussed below).
IntegrationTestAbstract

In fact, we recommend that your base class inherit from Isis' IntegrationTestAbstract class:

public abstract class SimpleAppIntegTest extends IntegrationTestAbstract {
    ...
}

Although not mandatory, this provides a number of helper/convenience methods and JUnit rules:

    @Rule
    public IsisTransactionRule isisTransactionRule =                         (1)
        new IsisTransactionRule();
    @Rule
    public JUnitRuleMockery2 context =                                       (2)
        JUnitRuleMockery2.createFor(Mode.INTERFACES_AND_CLASSES);
    @Rule
    public ExpectedException expectedExceptions =                            (3)
        ExpectedException.none();
    @Rule
    public ExceptionRecognizerTranslate exceptionRecognizerTranslations =    (4)
        ExceptionRecognizerTranslate.create();
1 ensures an Isis session/transaction running for each test
2 sets up a JMock context (using Isis' extension to JMock as described in JMock Extensions
3 standard JUnit rule for writing tests that throw exceptions
4 to capture messages that require translation, as described in i18 support.

All of these rules could be inlined in your own base class; as we say, they are a convenience.

In addition, IntegrationTestAbstract provides a number of helper/convenience methods. Most important amongst these are nextSession() and nextTransaction() that to allow tests to simulate multiple separate user interactions (each of which would normally occur in a separate transaction).

It also provides wrap(…​) and unwrap(…​) which delegate to the WrapperFactory. We talk about these methods in the next section

12.3.3. Wrapper Factory

The WrapperFactory service is responsible for wrapping a domain object in a dynamic proxy, of the same type as the object being proxied. And the role of this wrapper is to simulate the UI.

WrapperFactory uses javassist to perform its magic; this is the same technology that ORMs such as Hibernate use to manage lazy loading/dirty tracking (DataNucleus uses a different mechanism).

It does this by allowing through method invocations that would be allowed if the user were interacting with the domain object through one of the viewers, but throwing an exception if the user attempts to interact with the domain object in a way that would not be possible if using the UI.

The mechanics are as follows:

  1. the integration test calls the WrapperFactory to obtain a wrapper for the domain object under test. This is usually done in the test’s setUp() method.

  2. the test calls the methods on the wrapper rather than the domain object itself

  3. the wrapper performs a reverse lookup from the method invoked (a regular java.lang.reflect.Method instance) into the Isis metamodel

  4. (like a viewer), the wrapper then performs the "see it/use it/do it" checks, checking that the member is visible, that it is enabled and (if there are arguments) that the arguments are valid

  5. if the business rule checks pass, then the underlying member is invoked. Otherwise an exception is thrown.

The type of exception depends upon what sort of check failed. It’s straightforward enough: if the member is invisible then a HiddenException is thrown; if it’s not usable then you’ll get a DisabledException, if the args are not valid then catch an InvalidException.

wrapper factory

Let’s look in a bit more detail at what the test can do with the wrapper.

Wrapping and Unwrapping

Wrapping a domain object is very straightforward; simply call WrapperFactory#wrap(…​).

For example:

Customer customer = ...;
Customer wrappedCustomer = wrapperFactory.wrap(wrappedCustomer);

Going the other way — getting hold of the underlying (wrapped) domain object — is just as easy; just call WrapperFactory#unwrap(…​).

For example:

Customer wrappedCustomer = ...;
Customer customer = wrapperFactory.unwrap(wrappedCustomer);

If you prefer, you also can get the underlying object from the wrapper itself, by downcasting to WrappingObject and calling __isis_wrapped() method:

Customer wrappedCustomer = ...;
Customer customer = (Customer)((WrappingObject)wrappedCustomer).__isis_wrapped());

We’re not sure that’s any easier (in fact we’re certain it looks rather obscure). Stick with calling unwrap(…​)!

Using the wrapper

As the wrapper is intended to simulate the UI, only those methods that correspond to the "primary" methods of the domain object’s members are allowed to be called. That means:

  • for object properties the test can call the getter or setter method

  • for object collections the test can call the getter.

    If there is a supporting addTo…​() or removeFrom…​() method, then these can be called. It can also call add(…​) or remove(…​) on the collection (returned by the gettter) itself.

    In this respect the wrapper is more functional than the Wicket viewer (which does not expose the ability to mutate collections directly).

  • for object actions the test can call the action method itself.

As a convenience, we also allow the test to call any default…​(),choices…​() or autoComplete…​() method. These are often useful for obtaining a valid value to use.

What the test can’t call is any of the remaining supporting methods, such as hide…​(), disable…​() or validate…​(). That’s because their value is implied by the exception being thrown.

The wrapper does also allow the object’s title() method or its toString() , however this is little use for objects whose title is built up using the @Title annotation. Instead, we recommend that your test verifies an object’s title by calling DomainObjectContainer#titleOf(…​) method.

Firing Domain Events

As well as enforcing business rules, the wrapper has another important feature, namely that it will cause domain events to be fired.

For example, if we have an action annotated with @Action(domainEvent=…​):

public class ToDoItem ... {
    @Action(
            domainEvent =CompletedEvent.class
    )
    public ToDoItem completed() { ... }
    ...
}

then invoking the action through the proxy will cause the event (CompletedEvent above) to be fired to any subscribers. A test might therefore look like:

@Inject
private EventBusService eventBusService;                                          (1)

@Test
public void subscriberReceivesEvents() throws Exception {

    // given
    final ToDoItem.CompletedEvent[] evHolder = new ToDoItem.CompletedEvent[1];    (2)
    eventBusService.register(new Object() {
        @Subscribe
        public void on(final ToDoItem.CompletedEvent ev) {                        (3)
            evHolder[0] = ev;
        }
    });

    // when
    toDoItem.completed();                                                         (4)

    // then
    then(evHolder[0].getSource()).isEqualTo(unwrap(toDoItem));                    (5)
    then(evHolder[0].getIdentifier().getMemberName()).isEqualTo("completed");
}
1 inject EventBusService into this test
2 holder for subscriber to capture event to
3 subscriber’s callback, using the guava subscriber syntax
4 invoking the domain object using the wrapper
5 assert that the event was populated

The wrapper will also fire domain events for properties (if annotated with @Property(domainEvent=…​)) or collections (if annotated with @Collection(domainEvent=…​)).

It isn’t possible to use the WrapperFactory in a unit test, because there needs to be a running instance of Isis that holds the metamodel.

12.3.4. Maven Configuration

Apache Isis' integration test support is automatically configured if you use the SimpleApp archetype. To set it up manually, update the pom.xml of your domain object model module:

<dependency>
    <groupId>org.apache.isis.core</groupId>
    <artifactId>isis-core-integtestsupport</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.isis.core</groupId>
    <artifactId>isis-core-wrapper</artifactId>
</dependency>

We also recommend that you configure the maven-surefire-plugin to pick up the following class patterns:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.10</version>
    <configuration>
        <includes>
            <include>**/*Test.java</include>
            <include>**/*Test_*.java</include>
            <include>**/*Spec*.java</include>
        </includes>
        <excludes>
            <exclude>**/Test*.java</exclude>
            <exclude>**/*ForTesting.java</exclude>
            <exclude>**/*Abstract*.java</exclude>
        </excludes>
        <useFile>true</useFile>
        <printSummary>true</printSummary>
        <outputDirectory>${project.build.directory}/surefire-reports</outputDirectory>
    </configuration>
</plugin>

12.4. BDD Spec Support

Behaviour-driven design (BDD) redefines testing not as an after-the-fact "let’s check the system works", but rather as a means to work with the domain expert to (a) specify the behaviour of the feature before starting implementation, and (b) provide a means that the domain expert can verify the feature after it has been implemented.

Since domain experts are usually non-technical (at least, they are unlikely to be able to read or want to learn how to read JUnit/Java code), then applying BDD typically requires writing specifications in using structured English text and (ASCII) tables. The BDD tooling parses this text and uses it to actually interact with the system under test. As a byproduct the BDD frameworks generate readable output of some form; this is often an annotated version of the original specification, marked up to indicate which specifications passed, which have failed. This readable output is a form of "living documentation"; it captures the actual behaviour of the system, and so is guaranteed to be accurate.

There are many BDD tools out there; Apache Isis provides an integration with Cucumber JVM (see also the github site):

12.4.1. How it works

At a high level, here’s how Cucumber works

  • specifications are written in the Gherkin DSL, following the "given/when/then" format.

  • Cucumber-JVM itself is a JUnit runner, and searches for feature files on the classpath.

  • These in turn are matched to step definitions through regular expressions.

    It is the step definitions (the "glue") that exercise the system.

The code that goes in step definitions is broadly the same as the code that goes in an integration test method. One benefit of using step definitions (rather than integration tests) is that the step definitions are reusable across scenarios, so there may be less code overall to maintain. For example, if you have a step definition that maps to "given an uncompleted todo item", then this can be used for all the scenarios that start with that as their precondition.

12.4.2. Key classes

There are some key framework classes that make up the spec support; these are discussed below.

some of these are also used by Isis' Integration Test support.
IsisSystemForTest

The IsisSystemForTest class allows a complete running instance of Isis to be bootstrapped (with the JDO objectstore); this is then held on a a ThreadLocal from one test to another.

Typically bootstrapping code is used to lazily instantiate the IsisSystemForTest once and once only. The mechanism for doing this is line-for-line identical in both BDD step defs and integration tests.

ScenarioExecution

The ScenarioExecution provides a context for a scenario that is being executed. It is Cucumber that determines which step definitions are run, and in which order, and so state cannot be passed between step definitions using local variables or instance variables. Instead the ScenarioExecution acts like a hashmap, allowing each step to put data (eg "given an uncompleted todoitem") into the map or get data ("when I complete the todoitem") from the map. This is done using the putVar(…​) and getVar(…​) methods.

This corresponds broadly to the "World" object in Ruby-flavoured Cucumber.

The ScenarioExecution also provids access to the configured domain services (using the service(…​) method) and the DomainObjectContainer (through the container() method).

This could probably be refactored; Cucumber JVM provides automatic dependency injection into setp definitions, but Isis does not currently leverage or exploit this capability.

Like the IsisSystemForTest class, the ScenarioExecution class binds an instance of itself onto a ThreadLocal. It can then be accessed in BDD step definitions using ScenarioExecution.current() static method.

WrapperFactory

As with integration tests, the UI can be simulated by "wrapping" each domain object in a proxy using the WrapperFactory.

CukeGlueAbstract

The CukeGlueAbstract acts as a convenience superclass for writing BDD step definitions (analogous to the IntegrationTestAbstract for integation tests). Underneath the covers it delegates to an underlying ScenarioExecution.

12.4.3. Writing a BDD spec

BDD specifications contain:

  • a XxxSpec.feature file, describing the feature and the scenarios (given/when/then)s that constitute its acceptance criteria

  • a RunSpecs.java class file to run the specification (all boilerplate). This will run all .feature files in the same package or subpackages.

  • one or several XxxGlue constituting the step definitions to be matched against.

    The "glue" (step definitions) are intended to be reused across features. We therefore recommend that they reside in a separate package, and are organized by the entity type upon which they act.

    For example, given a feature that involves Customer and Order, have the step definitions pertaining to Customer reside in CustomerGlue, and the step definitions pertaining to Order reside in OrderGlue.

    The glue attribute of the Cucumber-JVM JUnit runner eallows you to indicate which package(s) should be recursively searched to find any glue.

  • a system initializer class. You can reuse the system initializer from any integration tests (as described in Integration Test Support, bootstrapping section).

Here’s an example of a feature from the SimpleApp archetype:

@SimpleObjectsFixture
Feature: List and Create New Simple Objects

  @integration
  Scenario: Existing simple objects can be listed and new ones created
    Given there are initially 3 simple objects
    When  I create a new simple object
    Then  there are 4 simple objects

The @SimpleObjectsFixture is a custom tag we’ve specified to indicate the prerequisite fixtures to be loaded; more on this in a moment. The @integration tag, meanwhile, says that this feature should be run with integration-level scope.

Although BDD specs are most commonly used for end-to-end tests (ie at the same scope as an integration test), the two concerns (expressability of a test to a business person vs granularity of the test) should not be conflated. There are a couple of good blog posts discussing this. The basic idea is to avoid the overhead of a heavy-duty integration test if possible.

Apache Isis does also support running BDD specs in unit test mode; by annotating the scenario with the @unit (rather than @integration tag). When running under unit-level scope, the Isis system is not instantiated. Instead, the ScenarioExecution class returns JMock mocks (except for the WrapperFactory, if configured).

To support unit testing scope Isis provides the InMemoryDB class; a glorified hashmap of "persisted" objects. Use of this utility class is optional.

Writing a BDD spec that supports both modes of operation therefore takes more effort and we expect most users interested in BDD will use integration-testing scope; for these reasons we have chosen not to include unit-testing support in the SimpleApp archetype. For those who do require faster-executing test suite, it’s worthwhile knowing that Isis can support this.

The RunSpecs class to run this feature (and any other features in this package or subpackages) is just boilerplate

@RunWith(Cucumber.class)
@CucumberOptions(
        format = {
                "html:target/cucumber-html-report"
                ,"json:target/cucumber.json"
        },
        glue={"classpath:domainapp.integtests.specglue"},
        strict = true,
        tags = { "~@backlog", "~@ignore" })
public class RunSpecs {
    // intentionally empty
}

The JSON formatter allows integration with enhanced reports, for example as provided by Masterthought.net (screenshots at end of page). (Commented out) configuration for this is provided in the SimpleApp archetype.

The bootstrapping of Isis can be moved into a BootstrappingGlue step definition:

public class BootstrappingGlue extends CukeGlueAbstract {
    @Before(value={"@integration"}, order=100)
    public void beforeScenarioIntegrationScope() {
        org.apache.log4j.PropertyConfigurator.configure("logging.properties");
        SimpleAppSystemInitializer.initIsft();

        before(ScenarioExecutionScope.INTEGRATION);
    }
    @After
    public void afterScenario(cucumber.api.Scenario sc) {
        assertMocksSatisfied();
        after(sc);
    }
}

The fixture to run also lives in its own step definition, CatalogOfFixturesGlue:

public class CatalogOfFixturesGlue extends CukeGlueAbstract {
    @Before(value={"@integration", "@SimpleObjectsFixture"}, order=20000)
    public void integrationFixtures() throws Throwable {
        scenarioExecution().install(new RecreateSimpleObjects());
    }
}

Note that this is annotated with a tag (@SimpleObjectsFixture) so that the correct fixture runs. (We might have a whole variety of these).

The step definitions pertaining to SimpleObject domain entity then reside in the SimpleObjectGlue class. This is where the heavy lifting gets done:

public class SimpleObjectGlue extends CukeGlueAbstract {
    @Given("^there are.* (\\d+) simple objects$")
    public void there_are_N_simple_objects(int n) throws Throwable {
        try {
            final List<SimpleObject> findAll = service(SimpleObjects.class).listAll();
            assertThat(findAll.size(), is(n));
            putVar("list", "all", findAll);

        } finally {
            assertMocksSatisfied();
        }
    }
    @When("^I create a new simple object$")
    public void I_create_a_new_simple_object() throws Throwable {
        service(SimpleObjects.class).create(UUID.randomUUID().toString());
    }
}

If using Java 8, note that Cucumber JVM supports a simplified syntax using lambdas.

12.4.4. BDD Tooling

To help write feature files and generate step definitions, we recommend Roberto Lo Giacco’s Eclipse plugin. For more information, see Dan’s short blog post. It works very well. Of interest: this is implemented using XText.

12.4.5. Maven Configuration

Apache Isis' BDD spec support is automatically configured if you use the SimpleApp archetype. To set it up manually, update the pom.xml of your domain object model module:

<dependency>
    <groupId>org.apache.isis.core</groupId>
    <artifactId>isis-core-specsupport</artifactId>
    <scope>test</scope> (1)
</dependency>
1 Normally test; usual Maven scoping rules apply.

We also recommend that you configure the maven-surefire-plugin to pick up the following class patterns:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.10</version>
    <configuration>
        <includes>
            <include>**/*Test.java</include>
            <include>**/*Test_*.java</include>
            <include>**/*Spec*.java</include>
        </includes>
        <excludes>
            <exclude>**/Test*.java</exclude>
            <exclude>**/*ForTesting.java</exclude>
            <exclude>**/*Abstract*.java</exclude>
        </excludes>
        <useFile>true</useFile>
        <printSummary>true</printSummary>
        <outputDirectory>${project.build.directory}/surefire-reports</outputDirectory>
    </configuration>
</plugin>

You may also find it more convenient to place the .feature files in src/test/java, rather than src/test/resources. If you wish to do this, then your integtest module’s pom.xml must contain:

<build>
    <testResources>
        <testResource>
            <filtering>false</filtering>
            <directory>src/test/resources</directory>
        </testResource>
        <testResource>
            <filtering>false</filtering>
            <directory>src/test/java</directory>
            <includes>
                <include>**</include>
            </includes>
            <excludes>
                <exclude>**/*.java</exclude>
            </excludes>
        </testResource>
    </testResources>
</build>

12.5. Fixture Scripts

When writing integration tests (and implementing the glue for BDD specs) it can be difficult to keep the "given" short; there could be a lot of prerequisite data that needs to exist before you can actually exercise your system. Moreover, however we do set up that data, but we also want to do so in a way that is resilient to the system changing over time.

On a very simple system you could probably get away with using SQL to insert directly into the database, or you could use a toolkit such as dbunit to upload data from flat files. Such approaches aren’t particularly maintainable though. If in the future the domain entities (and therefore corresponding database tables) change their structure, then all of those data sets will need updating.

Even more significantly, there’s no way to guarantee that the data that’s being loaded is logically consistent with the business behaviour of the domain objects themselves. That is, there’s nothing to stop your test from putting data into the database that would be invalid if one attempted to add it through the app.

The solution that Isis provides is a small library called fixture scripts. A fixture script is basically a wrapper for executing arbitrary work, but that work almost always invoking a business action.

If you want to learn more on this topic (with live coding!), check out this presentation given at BDD Exchange 2014.

There is another benefit to Isis' fixture script approach; the fixtures can be (in prototyping mode) run from your application. This means that fixture scripts can actually help all the way through the development lifecycle:

  • when specifying a new feature, you can write a fixture script to get the system into the "given" state, and then start exploring the required functionality with the domain expert actually within the application

    And if you can’t write a fixture script for the story, it probably means that there’s some prerequisite feature that needs implementing that you hadn’t previously recognized

  • when the developer implements the story, s/he has a precanned script to run when they manually verify the functionality works

  • when the developer automates the story’s acceptance test as an integration test, they already have the "given" part of the test implemented

  • when you want to pass the feature over to the QA/tester for additional manual exploratory testing, they have a fixture script to get them to a jumping off point for their explorations

  • when you want to demonstrate the implemented feature to your domain expert, your demo can use the fixture script so you don’t bore your audience in performing lots of boring setup before getting to the actual feature

  • when you want to roll out training to your users, you can write fixture scripts as part of their training exercises

The following sections explain the API and how to go about using the API.

12.5.1. API and Usage

There are two parts to using fixture scripts: the FixtureScripts domain service class, and the FixtureScript view model class, both being abstract.

  • The job of the FixtureScripts domain service is to locate all fixture scripts from the classpath and to let them be invoked, either from an integration test/BDD spec or from the UI of your Isis app.

  • With FixtureScript meanwhile you subclass for each of the scenarios that you want to define. You can also subclass from FixtureScript to create helpers; more on this below.

Let’s look at FixtureScripts domain service in more detail first.

FixtureScripts

In your code you subclass FixtureScripts domain service primarily just to specify which package to search for. For example, here’s the service that’s generated by the SimpleApp archetype:

@DomainService
@DomainServiceLayout(
        menuBar = DomainServiceLayout.MenuBar.SECONDARY,           (1)
        named="Prototyping",                                       (2)
        menuOrder = "500"
)
public class DomainAppFixturesService extends FixtureScripts {     (3)

    public DomainAppFixturesService() {
        super(
            "domainapp",                                           (4)
            MultipleExecutionStrategy.EXECUTE);                    (5)
    }
    @Override
    public FixtureScript default0RunFixtureScript() {
        return findFixtureScriptFor(RecreateSimpleObjects.class);  (6)
    }
    @Override
    public List<FixtureScript> choices0RunFixtureScript() {        (7)
        return super.choices0RunFixtureScript();
    }
}
1 in the UI, render the on the right hand side (secondary) menu bar…​
2 …​ under the "Prototyping" menu
3 inherit from org.apache.isis.applib.fixturescripts.FixtureScripts
4 search for all fixture scripts under the "domainapp" package; in your code this would probably be "com.mycompany.myapp" or similar
5 if the same fixture script (class) is encountered more than once, then run anyway; more on this in Organizing Fixture scripts.
6 (optional) specify a default fixture script to run, in this case RecreateSimpleObjects fixture script (see below)
7 make all fixture scripts found available as a drop-down (by increasing the visibility of this method to public)

Here’s how the domain service looks like in the UI:

prototyping menu

and here’s what the runFixtureScript action prompt looks like:

prompt

when this is executed, the resultant objects (actually, instances of FixtureResult`) are shown in the UI:

result list

If you had defined many fixture scripts then a drop-down might become unwieldy, in which case your code would probably override the autoComplete…​()) instead:

    @Override
    public List<FixtureScript> autoComplete0RunFixtureScript(final @MinLength(1) String searchArg) {
        return super.autoComplete0RunFixtureScript(searchArg);
    }

You are free, of course, to add additional "convenience" actions into it if you wish for the most commonly used/demo’d setups ; you’ll find that the SimpleApp archetype adds this additional action:

    @Action(
            restrictTo = RestrictTo.PROTOTYPING
    )
    @ActionLayout(
            cssClassFa="fa fa-refresh"
    )
    @MemberOrder(sequence="20")
    public Object recreateObjectsAndReturnFirst() {
        final List<FixtureResult> run = findFixtureScriptFor(RecreateSimpleObjects.class).run(null);
        return run.get(0).getObject();
    }

Let’s now look at the FixtureScript class, where there’s a bit more going on.

FixtureScript

A fixture script is ultimately just a block of code that can be executed, so it’s up to you how you implement it to set up the system. However, we strongly recommend that you use it to invoke actions on business objects, in essence to replay what a real-life user would have done. That way, the fixture script will remain valid even if the underlying implementation of the system changes in the future.

Here’s the RecreateSimpleObjects fixture script from the SimpleApp archetype:

public class RecreateSimpleObjects extends FixtureScript {                   (1)

    public final List<String> NAMES = Collections.unmodifiableList(Arrays.asList(
            "Foo", "Bar", "Baz", "Frodo", "Froyo",
            "Fizz", "Bip", "Bop", "Bang", "Boo"));                           (2)
    public RecreateSimpleObjects() {
        withDiscoverability(Discoverability.DISCOVERABLE);                   (3)
    }
    private Integer number;                                                  (4)
    public Integer getNumber() { return number; }
    public RecreateSimpleObjects setNumber(final Integer number) {
        this.number = number;
        return this;
    }
    private final List<SimpleObject> simpleObjects = Lists.newArrayList();   (5)
    public List<SimpleObject> getSimpleObjects() {
        return simpleObjects;
    }
    @Override
    protected void execute(final ExecutionContext ec) {          (6)
        // defaults
        final int number = defaultParam("number", ec, 3);        (7)
        // validate
        if(number < 0 || number > NAMES.size()) {
            throw new IllegalArgumentException(
                String.format("number must be in range [0,%d)", NAMES.size()));
        }
        // execute
        ec.executeChild(this, new SimpleObjectsTearDown());      (8)
        for (int i = 0; i < number; i++) {
            final SimpleObjectCreate fs =
                new SimpleObjectCreate().setName(NAMES.get(i));
            ec.executeChild(this, fs.getName(), fs);             (9)
            simpleObjects.add(fs.getSimpleObject());             (10)
        }
    }
}
1 inherit from org.apache.isis.applib.fixturescripts.FixtureScript
2 a hard-coded list of values for the names. Note that the Isis addons' fakedata module (non-ASF) could also have been used
3 whether the script is "discoverable"; in other words whether it should be rendered in the drop-down by the FixtureScripts service
4 input property: the number of objects to create, up to 10; for the calling test to specify, but note this is optional and has a default (see below). It’s important that a wrapper class is used (ie java.lang.Integer, not int)
5 output property: the generated list of objects, for the calling test to grab
6 the mandatory execute(…​) API
7 the defaultParam(…​) (inherited from FixtureScript) will default the number property (using Java’s Reflection API) if none was specified
8 call another fixture script (SimpleObjectsTearDown) using the provided ExecutionContext. Note that although the fixture script is a view model, it’s fine to simply instantiate it (rather than using DomainObjectContainer#newTransientInstance(…​)).
9 calling another fixture script (SimpleObjectCreate) using the provided ExecutionContext
10 adding the created object to the list, for the calling object to use.

Because this script has exposed a "number" property, it’s possible to set this from within the UI. For example:

prompt

When this is executed, the framework will parse the text and attempt to reflectively set the corresponding properties on the fixture result. So, in this case, when the fixture script is executed we actually get 6 objects created:

result list

It’s commonplace for one fixture script to call another. In the above example this script called SimpleObjectsTearDown and SimpleObjectCreate. Let’s take a quick look at SimpleObjectCreate:

public class SimpleObjectCreate extends FixtureScript {       (1)

    private String name;                                      (2)
    public String getName() { return name; }
    public SimpleObjectCreate setName(final String name) {
        this.name = name;
        return this;
    }
    private SimpleObject simpleObject;                        (3)
    public SimpleObject getSimpleObject() {
        return simpleObject;
    }
    @Override
    protected void execute(final ExecutionContext ec) {       (4)
        String name = checkParam("name", ec, String.class);   (5)
        this.simpleObject = wrap(simpleObjects)               (6)
                                .create(name);                (7)
        ec.addResult(this, simpleObject);                     (8)
    }
    @javax.inject.Inject
    private SimpleObjects simpleObjects;                      (9)
}
1 inherit from org.apache.isis.applib.fixturescripts.FixtureScript, as above
2 input property: name of the object; this time it is required
3 output property: the created simple object
4 the mandatory execute(…​) API
5 the checkParam(…​) (inherited from FixtureScript) will check that the "name" property has been populated (using Java’s Reflection API) and throw an exception if not
6 wrap the injected SimpleObjects domain service (using the WrapperFactory) to simulate interaction through the UI…​
7 .. and actually create the object using the "create" business action of that service
8 add the resulting object to the execution context; this makes the object available to access if run from the UI
9 inject the domain service into the fixture script
Using within Tests

Fixture scripts can be called from integration tests just the same way that fixture scripts can call one another.

For example, here’s an integration test from the SimpleApp archetype:

public class SimpleObjectIntegTest extends SimpleAppIntegTest {
    @Inject
    FixtureScripts fixtureScripts;                      (1)
    SimpleObject simpleObjectWrapped;
    @Before
    public void setUp() throws Exception {
        // given
        RecreateSimpleObjects fs =
             new RecreateSimpleObjects().setNumber(1);  (2)
        fixtureScripts.runFixtureScript(fs, null);      (3)

        SimpleObject simpleObjectPojo =
            fs.getSimpleObjects().get(0);               (4)
        assertThat(simpleObjectPojo).isNotNull();

        simpleObjectWrapped = wrap(simpleObjectPojo);   (5)
    }
    @Test
    public void accessible() throws Exception {
        // when
        final String name = simpleObjectWrapped.getName();
        // then
        assertThat(name).isEqualTo(fs.NAMES.get(0));
    }
    ...
}
1 inject the FixtureScripts domain service (just like any other domain service)
2 instantiate the fixture script for this test, and configure
3 execute the fixture script
4 obtain the object under test from the fixture
5 wrap the object (to simulate being interacted with through the UI)
Organizing Fixture scripts

There are lots of ways to organize fixture scripts, but we’ve used them as either:

These two styles are probably best illustrated with, well, some illustrations. Here’s a fixture script in the "flat" style for setting up a customer with some orders, a number of which has been placed:

flat 1

Notice how we have a single script that’s in control of the overall process, and takes responsibility for passing data from one fixture script to the next.

Here’s a similar, simpler script, from the same fictional app, to set up two customers:

flat 2

We can reuse the same fixture "customer" script, either calling it twice or (perhaps better) passing it an array of customer details to set up.

With the composite style, we rely on each fixture script to set up its own prerequisites. Thus:

composite

Back in the earlier section we noted the MultipleExecutionStrategy setting. We can now explain the meaning of this: the enum value of EXECUTE is designed for the flat style (where every fixture script will be called), whereas the enum value of IGNORE is designed for the composite style, and ensures that any fixture scripts visited more than once (eg TearDown) are only every executed the first time.

As already noted , the app generated by the SimpleApp archetype uses the flat structure, and we feel that it’s a better at separating out the "how" (how we set up some graph of domain objects into a known state, eg a customer with shipped placed orders and a newly placed order) from the "what" (what data should we actually use for the customer’s name, say).

The composite style tends to combine these, which one could argue does not separate responsibilities well enough. On the other hand, one could also make an argument that the composite style is a good way to implement precanned personas, eg "Joe", the customer who has a newly placed order, from "Mary" customer who has none.

12.5.2. SudoService

Sometimes in our fixture scripts we want to perform a business action running as a particular user. This might be for the usual reason - so that our fixtures accurately reflect the reality of the system with all business constraints enforced using the WrapperFactory - or more straightforwardly it might be simply that the action depends on the identity of the user invoking the action.

An example of the latter case is in the (non-ASF) Isis addons' todoapp's ToDoItem class:

Production code that depends on current user
public ToDoItem newToDo(
        @Parameter(regexPattern = "\\w[@&:\\-\\,\\.\\+ \\w]*") @ParameterLayout(named="Description")
        final String description,
        @ParameterLayout(named="Category")
        final Category category,
        @Parameter(optionality = Optionality.OPTIONAL) @ParameterLayout(named="Subcategory")
        final Subcategory subcategory,
        @Parameter(optionality = Optionality.OPTIONAL) @ParameterLayout(named="Due by")
        final LocalDate dueBy,
        @Parameter(optionality = Optionality.OPTIONAL) @ParameterLayout(named="Cost")
        final BigDecimal cost) {
    return newToDo(description, category, subcategory, currentUserName(), dueBy, cost);
}
private String currentUserName() {
    return container.getUser().getName(); (1)
}
1 is the current user.

The fixture for this can use the SudoService to run a block of code as a specified user:

Fixture Script
final String description = ...
final Category category = ...
final Subcategory subcategory = ...
final LocalDate dueBy = ...
final BigDecimal cost = ...
final Location location = ...

toDoItem = sudoService.sudo(username,
        new Callable<ToDoItem>() {
            @Override
            public ToDoItem call() {
                final ToDoItem toDoItem = wrap(toDoItems).newToDo(
                        description, category, subcategory, dueBy, cost);
                wrap(toDoItem).setLocation(location);
                return toDoItem;
            }
        });

Behind the scenes the SudoService simply talks to the DomainObjectContainer to override the user returned by the getUser() API. It is possible to override both users and roles.

13. Headless access

This chapter tackles the topic of enabling access to an Isis application directly, or at least, not through either the Wicket or Restful viewers.

There are several main use-cases:

  • enabling background execution, eg of a thread managed by Quartz scheduler and running within the webapp

  • integration from other systems, eg for a subscriber on a pub/sub mechanism such as Camel, pushing changes through an Isis domain model.

  • leveraging an Isis application within a batch process

Note that the calling thread runs in the same process space as the Isis domain object model (must be physically linked to the JAR files containing the domain classes). For use cases where the calling thread runs in some other process space (eg migrating data from a legacy system), then the Restful Objects viewer is usually the way to go.

The API described in this chapter is reasonably low-level, allowing code to interact very directly with the Isis metamodel and runtime. Such callers should be considered trusted: they do not (by default) honour any business rules eg implicit in the Isis annotations or hide/disable/validate methods. However the [WrapperFactory] service could be used to enforce such business rules if required.

13.1. AbstractIsisSessionTemplate

The AbstractIsisSessionTemplate class (whose name is inspired by the Spring framework’s naming convention for similar classes that query JDBC, JMS, JPA etc.) provides the mechanism to open up a 'session' within the Isis framework, in order to resolve and interact with entities.

The class itself is intended to be subclassed:

public abstract class AbstractIsisSessionTemplate {

    public void execute(final AuthenticationSession authSession, final Object context) { ... } (1)
    protected abstract void doExecute(Object context); (2)

    protected ObjectAdapter adapterFor(final Object targetObject) { ... }
    protected ObjectAdapter adapterFor(final RootOid rootOid) { ... }

    protected PersistenceSession getPersistenceSession() { ... }
    protected IsisTransactionManager getTransactionManager() { ... }
    protected AdapterManager getAdapterManager() { ... }
}
1 execute(…​) sets up the IsisSession and delegates to …​
2 doExecute(…​), the mandatory hook method for subclasses to implement. The passed object represents passes a context from the caller (eg the scheduler, cron job, JMS etc) that instantiated and executed the class.

The protected methods expose key internal APIs within Isis, for the subclass to use as necessary.

One notable feature of AbstractIsisSessionTemplate is that it will automatically inject any domain services into itself. Thus, it is relatively easy for the subclass to "reach into" the domain, through injected repository services.

13.2. BackgroundCommandExecution

The BackgroundCommandExecution class (a subclass of AbstractIsisSessionTemplate) is intended to simplify the execution of background Commands persisted by way of the [CommandService] and the [BackgroundCommandService].

Its signature is:

public abstract class BackgroundCommandExecution extends AbstractIsisSessionTemplate {
    protected void doExecute(Object context) { ... }
    protected abstract List<? extends Command> findBackgroundCommandsToExecute(); (1)
}
1 findBackgroundCommandsToExecute() is a mandatory hook method for subclasses to implement.

This allows for different implementations of the CommandService and BackgroundCommandService to persist to wherever.

The diagram below (yuml.me/363b335f) shows the dependencies between these various classes:

BackgroundCommandExecution
Figure 5. Inheritance Hierarchy for BackgroundCommandExecution

13.2.1. Background Execution

The BackgroundCommandExecutionFromBackgroundCommandServiceJdo is a concrete subclass of BackgroundCommandExecution (see the BackgroundCommandService), the intended use being for the class to be instantiated regularly (eg every 10 seconds) by a scheduler such as Quartz) to poll for Commands to be executed, and then execute them.

This implementation queries for Commands persisted by the Isis addons Command Module's implementations of CommandService and BackgroundCommandService using the BackgroundCommandServiceJdoRepository.

The diagram below (yuml.me/25343da1) shows the inheritance hierarchy for this class:

BackgroundCommandExecutionFromBackgroundCommandServiceJdo
Figure 6. Inheritance Hierarchy for BackgroundCommandExecutionFromBackgroundCommandServiceJdo

14. Extending

This chapter explains the main APIs for extending Apache Isis: the programming model conventions, the Wicket viewer, and the Restful Objects viewer.

14.1. Programming Model

This section describes three mechanisms you can use to extend or alter the programming conventions that Isis understands to build up its metamodel.

14.1.1. Custom validator

Apache Isis' programming model includes a validator component that detects and prevents (by failing fast) a number of situations where the domain model is logically inconsistent with itself.

For example, the validator will detect any orphaned supporting methods (eg hideXxx()) if the corresponding property or action has been renamed or deleted but the supporting method was not also updated. Another example is that a class cannot have a title specified both using title() method and also using @Title annotation.

The support for disallowing deprecated annotations is also implemented using the metamodel validator.

You can also impose your own application-specific rules by installing your own metamodel validator. To give just one example, you could impose naming standards such as ensuring that a domain-specific abbreviation such as "ISBN" is always consistently capitalized wherever it appears in a class member.

Isis' Maven plugin will also validate the domain object model during build time.

API and Implementation

There are several ways to go about implementing a validator.

MetaModelValidator

Any custom validator must implement Isis' internal MetaModelValidator interface, so the simplest option is just to implement MetaModelValidator directly:

public interface MetaModelValidator
        implements SpecificationLoaderSpiAware { (1)
    public void validate(
        ValidationFailures validationFailures);  (2)
}
1 the SpecificationLoader is the internal API providing access to the Isis metamodel.
2 add any metamodel violations to the ValidationFailures parameter (the collecting parameter pattern)
Visitor

More often than not, you’ll want to visit every element in the metamodel, and so for this you can instead subclass from MetaModelValidatorVisiting.Visitor:

public final class MetaModelValidatorVisiting ... {
    public static interface Visitor {
        public boolean visit(                       (1)
            ObjectSpecification objectSpec,         (2)
            ValidationFailures validationFailures); (3)
    }
    ...
}
1 return true continue visiting specs.
2 ObjectSpecification is the internal API representing a class
3 add any metamodel violations to the ValidationFailures parameter

You can then create your custom validator by subclassing MetaModelValidatorComposite and adding the visiting validator:

public class MyMetaModelValidator extends MetaModelValidatorComposite {
    public MyMetaModelValidator() {
        add(new MetaModelValidatorVisiting(new MyVisitor()));
    }
}

If you have more than one rule then each can live in its own visitor.

SummarizingVisitor

As a slight refinement, you can also subclass from MetaModelValidatorVisiting.SummarizingVisitor:

public final class MetaModelValidatorVisiting ... {
    public static interface SummarizingVisitor extends Visitor {
        public void summarize(ValidationFailures validationFailures);
    }
    ...
}

A SummarizingVisitor will be called once after every element in the metamodel has been visited. This is great for performing checks on the metamodel as a whole. For example, Isis uses this to check that there is at least one @Persistable domain entity defined.

Configuration

Once you have implemented your validator, you must register it with the framework by defining the appropriate configuration property:

isis.reflector.validator=com.mycompany.myapp.MyMetaModelValidator

14.1.2. Finetuning

The core metamodel defines APIs and implementations for building the Isis metamodel: a description of the set of entities, domain services and values that make up the domain model.

The description of each domain class consists of a number of elements:

ObjectSpecification

Analogous to java.lang.Class; holds information about the class itself and holds collections of each of the three types of class' members (below);

OneToOneAssociation

Represents a class member that is a single-valued property of the class. The property’s type is either a reference to another entity, or is a value type.

OneToManyAssociation

Represents a class member that is a collection of references to other entities. Note that Isis does not currently support collections of values.

ObjectAction

Represents a class member that is an operation that can be performed on the action. Returns either a single value, a collection of entities, or is void.

The metamodel is built up through the ProgrammingModel, which defines an API for registering a set of FacetFactorys. Two special FacetFactory implementations - PropertyAccessorFacetFactory and CollectionAccessorFacetFactory - are used to identify the class members. Pretty much all the other FacetFactorys are responsible for installing Facets onto the metamodel elements.

There are many many such Facets, and are used to do such things get values from properties or collections, modify properties or collections, invoke action, hide or disable class members, provide UI hints for class members, and so on. In short, the FacetFactorys registered defines the Isis programming conventions.

Modifying the Prog. Model

The default implementation of ProgrammingModel is ProgrammingModelFacetsJava5, which registers a large number of FacetFactorys.

By editing isis.properties you can modify the programming conventions either by (a) using the default programming model, but tweaking it to include new `FacetFactory`s or exclude existing, or (b) by specifying a completely different programming model implementation.

Let’s see how this is done.

Including or excluding facets

Suppose that you wanted to completely remove support for the (already deprecated) @ActionOrder annotation. This would be done using:

isis.reflector.facets.exclude=org.apache.isis.core.metamodel.facets.object.actionorder.annotation.ActionOrderFacetAnnotationFactory

Or, suppose you wanted to use the example "namefile" FacetFactory as part of your programming conventions, use:

isis.reflector.facets.include=org.apache.isis.example.metamodel.namefile.facets.NameFileFacetFactory

To include/exclude more than one FacetFactory, specify as a comma-separated list. And if you want to dig into this in more detail, the code that implements this logic is JavaReflectorInstallerNoDecorators.

This thread from the users mailing list (in Apr 2014) shows a typical customization (to enable per-instance security) (though note that Multi-Tenancy is now a better solution to that particular use-case.

Replacing the Prog. Model

If you want to make many changes to the programming model, then (rather than include/exclude lots of facet factories) you can specify a completely new programming model. For this you’ll first need an implementation of ProgrammingModel.

One option is to subclass from ProgammingModelFacetsJava5; in your subclass you could remove any FacetFactorys that you wanted to exclude, as well as registering your own implementations.

To tell Isis to use your new programming model, use:

isis.reflector.facets=com.mycompany.myapp.isis.IsisProgrammingModel

Again, the code that implements this logic is JavaReflectorInstallerNoDecorators.

14.1.3. Layout Metadata Reader

The metadata for domain objects is obtained both statically and dynamically.

The default implementation for reading dynamic layout metadata is org.apache.isis.core.metamodel.layoutmetadata.json.LayoutMetadataReaderFromJson, which is responsible for reading from the Xxx.layout.json files on the classpath (for each domain entity Xxx).

You can also implement your own metadata readers and plug them into Isis. These could read from a different file format, or they could, even, read data dynamically from a URL or database. (Indeed, one could imagine an implementation whereby users could share layouts, all stored in some central repository).

API and Implementation

Any reader must implement Isis' internal LayoutMetadataReader interface:

public interface LayoutMetadataReader {
    public Properties asProperties(Class<?> domainClass) throws ReaderException;
}

The implementation "simply" returns a set of properties where the property key is a unique identifier to both the class member and also the facet of the class member to which the metadata relates.

See the implementation of the built-in LayoutMetadataReaderFromJson for more detail.

Configuration

Once you have implemented your validator, you must register it with the framework by defining the appropriate configuration property:

isis.reflector.layoutMetadataReaders=\
           com.mycompany.myapp.MyMetaModelValidator,\
           org.apache.isis.core.metamodel.layoutmetadata.json.LayoutMetadataReaderFromJson   (1)
1 the property replaces any existing metadata readers; if you want to preserve the ability to read from Xxx.layout.json then also register Isis' built-in implementation.

14.2. Wicket viewer

The Wicket viewer allows you to customize the GUI in several (progressively more sophisticated) ways:

The first two of these options are discussed in the Wicket Viewer chapter. This chapter describes the remaining "heavier-weight/more powerful" options.

14.2.1. Custom Bootstrap theme

The Isis Wicket viewer uses Bootstrap styles and components (courtesy of the Wicket Bootstrap integration).

By default the viewer uses the default bootstrap theme. It is possible to configure the Wicket viewer to allow the user to select other themes provided by bootswatch.com, and if required one of these can be set as the default.

However, you may instead want to write your own custom theme, for example to fit your company’s look-n-feel/interface guidelines. This is done by implementing Wicket Bootstrap’s de.agilecoders.wicket.core.settings.ITheme class. This defines:

  • the name of the theme

  • the resources it needs (the CSS and optional JS and/or fonts), and

  • optional urls to load them from a Content Delivery Network (CDN).

To make use of the custom ITheme the application should register it by adding the following snippet in (your application’s subclass of) IsisWicketApplication:

public void init() {
    ...
    IBootstrapSettings settings = new BootstrapSettings();
    ThemeProvider themeProvider = new SingleThemeProvider(new MyTheme());
    settings.setThemeProvider(themeProvider);
    Bootstrap.install(getClass(), settings);
}

14.2.2. Replacing page elements

Replacing elements of the page is the most powerful general-purpose way to customize the look-n-feel of the viewer. Examples at the (non-ASF) Isis Add-ons include the gmap3 extension, the calendar extension, the excel download and the wickedcharts charting integration.

The pages generated by Isis' Wicket viewer are built up of numerous elements, from fine-grained widgets for property/parameter fields, to much larger components that take responsibility for rendering an entire entity entity, or a collection of entities. Under the covers these are all implementations of the the Apache Wicket Component API. The larger components delegate to the smaller, of course.

How the viewer selects components

Components are created using Isis' ComponentFactory interface, which are registered in turn through the ComponentFactoryRegistrar interface. Every component is categorizes by type (the ComponentType enum), and Isis uses this to determine which ComponentFactory to use. For example, the ComponentType.BOOKMARKED_PAGES is used to locate the ComponentFactory that will build the bookmarked pages panel.

Each factory is also handed a model (an implementation of org.apache.wicket.IModel) appropriate to its ComponentType; this holds the data to be rendered. For example, ComponentType.BOOKMARKED_PAGES is given a BookmarkedPagesModel, while ComponentType.SCALAR_NAME_AND_VALUE factories are provided a model of type of type ScalarModel .

In some cases there are several factories for a given ComponentType; this is most notably the case for ComponentType.SCALAR_NAME_AND_VALUE. After doing a first pass selection of candidate factories by ComponentType, each factory is then asked if it appliesTo(Model). This is an opportunity for the factory to check the model itself to see if the data within it is of the appropriate type.

Thus, the BooleanPanelFactory checks that the ScalarModel holds a boolean, while the JodaLocalDatePanelFactory checks to see if it holds org.joda.time.LocalDate.

There will typically be only one ComponentFactory capable of rendering a particular ComponentType/ScalarModel combination; at any rate, the framework stops as soon as one is found.

There is one refinement to the above algorithm where multiple component factories might be used to render an object; this is discussed in Additional Views of Collections, below.

How to replace a component

This design (the chain of responsibility design pattern) makes it quite straightforward to change the rendering of any element of the page. For example, you might switch out Isis' sliding bookmark panel and replace it with one that presents the bookmarks in some different fashion.

First, you need to write a ComponentFactory and corresponding Component. The recommended approach is to start with the source of the Component you want to switch out. For example:

public class MyBookmarkedPagesPanelFactory extends ComponentFactoryAbstract {
    public MyBookmarkedPagesPanelFactory() {
        super(ComponentType.BOOKMARKED_PAGES);
    }
    @Override
    public ApplicationAdvice appliesTo(final IModel<?> model) {
        return appliesIf(model instanceof BookmarkedPagesModel);
    }
    @Override
    public Component createComponent(final String id, final IModel<?> model) {
        final BookmarkedPagesModel bookmarkedPagesModel = (BookmarkedPagesModel) model;
        return new MyBookmarkedPagesPanel(id, bookmarkedPagesModel);
    }
}

and

public class MyBookmarkedPagesPanel
    extends PanelAbstract<BookmarkedPagesModel> {
   ...
}

Here PanelAbstract ultimately inherits from org.apache.wicket.Component. Your new Component uses the information in the provided model (eg BookmarkedPagesModel) to know what to render.

Next, you will require a custom implementation of the ComponentFactoryRegistrar that registers your custom ComponentFactory as a replacement:

@Singleton
public class MyComponentFactoryRegistrar extends ComponentFactoryRegistrarDefault {
    @Override
    public void addComponentFactories(ComponentFactoryList componentFactories) {
        super.addComponentFactories(componentFactories);
        componentFactories.add(new MyBookmarkedPagesPanelFactory());
    }
}

This will result in the new component being used instead of (that is, discovered prior to) Isis' default implementation.

Previously we suggested using "replace" rather than "add"; however this has unclear semantics for some component types; see ISIS-996.

Finally (as for other customizations), you need to adjust the Guice bindings in your custom subclass of IsisWicketApplication:

public class MyAppApplication extends IsisWicketApplication {
    @Override
    protected Module newIsisWicketModule() {
        final Module isisDefaults = super.newIsisWicketModule();
        final Module myAppOverrides = new AbstractModule() {
            @Override
            protected void configure() {
                ...
                bind(ComponentFactoryRegistrar.class)
                    .to(MyComponentFactoryRegistrar.class);
                ...
            }
        };

        return Modules.override(isisDefaults).with(myAppOverrides);
    }
}
Additional Views of Collections

As explained above, in most cases Isis' Wicket viewer will search for the first ComponentFactory that can render an element, and use it. In the case of (either standalone or parented) collections, though, Isis will show all available views.

For example, out-of-the-box Isis provides a table view, a summary view (totals/sums/averages of any data), and a collapsed view (for @Render(LAZILY) collections). These are selected by clicking on the toolbar by each collection.

Additional views though could render the objects in the collection as a variety of ways. Indeed, some third-party implementations already exist:

Registering these custom views is just a matter of adding the appropriate Maven module to the classpath. Isis uses the JDK ServiceLoader API to automatically discover and register the ComponentFactory of each such component.

If you want to write your own alternative component and auto-register, then include a file META-INF/services/org.apache.isis.viewer.wicket.ui.ComponentFactory whose contents is the fully-qualified class name of the custom ComponentFactory that you have written.

Wicket itself has lots of components available at its wicketstuff.org companion website; you might find some of these useful for your own customizations.

Custom object view (eg dashboard)

One further use case in particular is worth highlighting; the rendering of an entire entity. Normally entities this is done using EntityCombinedPanelFactory, this being the first ComponentFactory for the ComponentType.ENTITY that is registered in Isis default ComponentFactoryRegistrarDefault.

You could, though, register your own ComponentFactory for entities that is targeted at a particular class of entity - some sort of object representing a dashboard, for example. It can use the EntityModel provided to it to determine the class of the entity, checking if it is of the appropriate type. Your custom factory should also be registered before the EntityCombinedPanelFactory so that it is checked prior to the default EntityCombinedPanelFactory:

@Singleton
public class MyComponentFactoryRegistrar extends ComponentFactoryRegistrarDefault {
    @Override
    public void addComponentFactories(ComponentFactoryList componentFactories) {
        componentFactories.add(new DashboardEntityFactory());
        ...
        super.addComponentFactories(componentFactories);
        ...
    }
}

14.2.3. Custom pages

In the vast majority of cases customization should be sufficient by replacing elements of a page. However, it is also possible to define an entirely new page for a given page type.

Isis defines eight page types (see the org.apache.isis.viewer.wicket.model.models.PageType enum):

Table 9. PageType enum
Page type Renders

SIGN_IN

The initial sign-in (aka login) page

SIGN_UP

The sign-up page (if user registration is enabled).

SIGN_UP_VERIFY

The sign-up verification page (if user registration is enabled; as accessed by link from verification email)

PASSWORD_RESET

The password reset page (if enabled).

HOME

The home page, displaying either the welcome message or dashboard

ABOUT

The about page, accessible from link top-right

ENTITY

Renders a single entity or view model

STANDALONE_COLLECTION

Page rendered after invoking an action that returns a collection of entites

VALUE

After invoking an action that returns a value type (though not URLs or Blob/Clobs, as these are handled appropriately automatically).

VOID_RETURN

After invoking an action that is void

ACTION_PROMPT

(No longer used).

The PageClassList interface declares which class (subclass of org.apache.wicket.Page is used to render for each of these types. For example, Isis' WicketSignInPage renders the signin page.

To specify a different page class, create a custom subclass of PageClassList:

@Singleton
public class MyPageClassList extends PageClassListDefault {
    protected Class<? extends Page> getSignInPageClass() {
        return MySignInPage.class;
    }
}

You then need to register your custom PageClassList. This is done by adjusting the Guice bindings (part of Isis' bootstrapping) in your custom subclass of IsisWicketApplication:

public class MyAppApplication extends IsisWicketApplication {
    @Override
    protected Module newIsisWicketModule() {
        final Module isisDefaults = super.newIsisWicketModule();
        final Module myAppOverrides = new AbstractModule() {
            @Override
            protected void configure() {
                ...
                bind(PageClassList.class).to(MyPageClassList.class);
                ...
            }
        };
        return Modules.override(isisDefaults).with(myAppOverrides);
    }
}

14.2.4. Isis Addons Extensions

TODO

Note that Isis addons, while maintained by Isis committers, are not part of the ASF.

14.3. Restful Objects viewer

The RestfulObjects viewer implements the Restful Object spec, meaning that it defines a well-defined set of endpoint URLs as resources, and generates a well-defined set of (JSON) representations when these resources are accessed.

Sometimes though you may want to extend or change the representations generated. This might be because you want to write a RESTful client that uses a particular library (say a Javascript library or web components) that can only handle representations in a certain form.

Or, you might want to have Isis generate representations according to some other "standard", of which there are many (see note).

Other "standards" for defining representations that we are aware of include: HAL (Mike Kelly), Collection+JSON (Mike Amundsen), Siren (Kevin Swiber), JSON API (Steve Klabnik), Hyper+JSON (Gregg Cainus), JSON-LD (W3C) and Hydra (Markus Lanthaler).

A good discussion about the relative merits of several of these different hypermedia formats can be found here.

Whatever your reasons, Isis provides several mechanisms by which you can change the generated representation. This section lists these, from the simplest (most limited) to the most flexible (more work).

  • Configuration Properties

    If all that is required is a very simple representations (of objects), you can reconfigure the RestfulObjects viewer to provide a simplified output, as described here.

    If you simply want to suppress certain elements, as described here.

  • RepresentationService

    The RepresentationService is an SPI domain service (plugin-point) that allows an arbitrary representation to be generated for any of the resources defined in the RO spec.

    The default implementation does, of course, generate the representations as defined in the RO spec.

  • ContentNegotiationService

    The ContentNegotiationService is very similar to RepresentationService, in that it allows for a different representation to be generated. It isn’t responsible for generating the default RO representations, however; if can simply return null (and leave it to the RepresentationService to generate the correct representations.

    The default implementation provides support for the x-ro-domain-type parameter for HTTP Accept headers, providing the capability sketched out in section 34.1 of the RO spec v1.0. As such, it delegates to ContentMappingService, described below.

  • ContentMappingService

    The ContentMappingService is used by the default implementation of ContentNegotationService to transform a domain object (usually an entity) into some other form (usually a DTO), as specified by the x-ro-domain-type parameter.

This diagram shows how these services collaborate:

service collaborations

Taken together these configuration properties and domain services offer a lot of flexibility in terms of the representations that can be generated from the RestfulObjects viewer. Please be aware though that these using these SPIs means that the representations generated by the viewer are no longer conformant with those of the RO spec.

15. Troubleshooting

TODO

15.1. Logging

15.2. Enabling Logging

Sometimes you just need to see what is going on. There are various ways in which logging can be enabled, here are the ones we tend to use.

15.2.1. In Apache Isis

Modify WEB-INF/logging.properties (a log4j config file)

15.2.2. In DataNucleus

As per the DN logging page

15.2.3. In the JDBC Driver

Configure log4jdbc JDBC rather than the vanilla driver (see WEB-INF/persistor_datanucleus.properties) and configure log4j logging (see WEB-INF/logging.properties).

There are examples of both in the SimpleApp archetype.

15.2.4. In the Database

HSQLDB Logging

Add ;sqllog=3 to the end of the JDBC URL.

PostgreSQL Logging

In postgresql\9.2\data\postgresql.conf:

Will then log to postgresql\9.2\data\pg_log directory.

Note that you must restart the service for this to be picked up.

MS SQL Server Logging

Use the excellent SQL Profiler tool.


Copyright © 2010~2015 The Apache Software Foundation, licensed under the Apache License, v2.0.
Apache, the Apache feather logo, Apache Isis, and the Apache Isis project logo are all trademarks of The Apache Software Foundation.

-->