7. Custom entity extensions

We will use an example integration with the Profields app by Deiser to help explain the concepts behind this type of extension. To limit the complexity of the example, we will assume the following simplifications:

  • The example will be focused on handling Profields belonging to four different types, layouts, and their relationship with projects.

  • Working "as if" a layout could be associated with just one project.

  • Using a slightly simplified model of the inner structure of a layout. We will only maintain the order of sections in a layout but not the order of containers in a column or of field items inside a container. We expect, however, that the part of the example that manages the ordering of sections will make it easier to implement a similar solution for containers or field items.

Have a logical model of the information you want to support

 Remember that implementing an extension to migrate a new kind of object that does not already exist in Jira (before your app is installed) consists primarily of declaring the structure of those objects. In a second stage, it involves implementing the operations to create, update, and, in some cases, delete those objects.

Focusing on the structure of the data, you should be able to answer questions like these:

  1. Which types of entities will be supported?

  2. Which of these are parts of bigger entities and cannot exist independently?

  3. Which properties should be migrated for each entity?

  4. Which of these properties represents a reference to other entities?

  5. A Jira admin using PCJ will move the configuration and/or data for one or several projects. In these situations, which items of your app should move with those projects?

The answer to each of these questions maps directly to concrete parts of the SPI. Do not be concerned if the precise answer to these questions is unclear now; we will revisit them in greater detail later in this document.

As a practical tip, before starting to create a custom entity extension, it is worthwhile creating a map of the entities to be extended with content similar to a data model diagram. Include each entity, annotate it with its properties (maybe with a brief note of what each property means), and draw relations between entities whenever a property in an entity references another entity. Also include use relations to/from built-in entities in Jira (projects, custom fields, workflows, etc.) Using the Profields example in a style similar to an E-R diagram will result in this model:

Relation model of entities to be extended

In some cases, it might be a matter of discussion for two related objects, whether A uses B or B uses A. We recommend following these patterns to make a decision:

  • If, for example, A contains a property, field, or attribute that is an identifier for B, but B does not contain a similar field/attribute with an identifier for A, we should say that "A uses B" or "A references B".

  • If A requires object B to be properly configured, or to be useful in the normal operation of Jira, then we should consider that "A uses B". For example, a project must have a workflow scheme (even if it is the default one) in order to create issues, transition them, etc. On the other hand, a workflow scheme can exist even if no project is linked to it. Most people will consider that projects use workflow schemes, not the other way around.

  • Think of how a typical Jira administrator would view the relationship. Try to model it in the way that will make the most sense to this typical administrator. This is important, as it will make the user experience clearer and more intuitive.

CustomEntity is the main abstraction in any app extension. Each instance of CustomEntity represents the common properties of the set of objects that belong to the same type/entity defined by the Profields app. In this example, we can see that there are five entities to be represented:

  • Layout

  • Section

  • Container

  • Field item

  • Profield

The next step is to decide for each entity if it represents objects that can exist on their own or if they can only appear as part of other objects.

GlobalCustomEntity

Some objects in Jira exist independently of other objects. For example, a project may exist without being part of anything else in Jira. The same applies to the object types represented in a PCJ extension. If objects of a given type can exist on their own, then they are instances of a GlobalCustomEntity. In this example, it is clear that both a layout and a "Profield" can be created without being inside any other object defined in the app, so their entities must be instances of GlobalCustomEntity:

Entity for Layouts

@Profile("pc4j-extensions") @Component public class LayoutEntity implements GlobalCustomEntity<Layout> { ....

 

Entity for Profields

@Profile("pc4j-extensions") @Component public class ProfieldEntity implements GlobalCustomEntity<Field> { ...

Note that GlobalCustomEntity has a type parameter that must be the Java class used to represent objects belonging to this entity, in these two cases com.deiser.jira.profields.api.layout.Layout and com.deiser.jira.profields.api.field.Field.

Names for the custom entity

Each CustomEntity has a name given by getTypeName(), which must be unique among all the CustomEntity objects belonging to the same app and must not contain a colon. Given these restrictions, concatenation of the app key, a colon, and the custom entity name will yield a name for the custom entity, which is unique among all PCJ extensions that could be installed in a Jira instance.

There is also a set of unique names for Jira built-in types, which are defined in com.awnaba.projectconfigurator.operationsapi.ObjectAlias, therefore, there is a globally unique name for any entity whether built-in or part of a PCJ extension.

Creating new objects

Any custom entity must define a method to create new objects of its type.

Creating New Layouts

@Override public Layout createNew(ObjectDescriptor<Layout> objectDescriptor) { LayoutBuilder builder = layoutService.getLayoutBuilder(); builder.setName(objectDescriptor.getPropertyValue(nameProperty)); builder.setDescription(objectDescriptor.getPropertyValue(descriptionProperty)); return layoutService.create(builder); }

This method receives an ObjectDescriptor. This is an object that contains all information relevant to the object about to be created. It can be queried for the value of any of the properties of that object (see more about properties in the next section).

Configuration or data

Any GlobalCustomEntity has a boolean property that specifies if the object is part of the configuration or the data.

Consider two Jira instances. In one scenario, both are production instances, and you want to move a project from one instance to the other (perhaps the line of business associated with that Jira project was sold to another company). In that case, the entire project, both its configuration (issue types, workflows, schemes, etc.) and its data (issues, attachments, comments) will be moved to the new instance. In a different scenario, the destination instance is a production one, and the source instance is for testing, where different changes to the project (perhaps to support new business processes) are being tested and validated by users. In this case, only the project configuration will be moved, as only those changes were made in testing and nobody would want to overwrite the live data in production.

Examining these scenarios will help you determine if a given object type must be moved in a configuration-only migration or not. PCJ supports both modes: migrating configuration only or configuration and data. If nothing is specified as a default, global entities are considered to be configuration items (not data).

In the current version of the integration framework, this property is ignored, and all entities are treated “as if” they were part of the configuration. If you need to support different treatments for your data and configuration entities, please contact us through our support channel.

Properties

A property is any attribute of an object that users want to migrate to a different instance of Jira.

Any CustomEntity has a method Collection<Property<T,?>> getProperties() that returns the properties of the entity that must be handled in the migration process. Usually, users will want to migrate persistent, visible properties of the objects. For example, they will not want to migrate IDs (not visible to users, not persistent across instances) or volatile information like cached results of a filter.

Returning to the Profields example, these properties can be associated with a Layout:

  • Name

  • Description

  • The project it is associated with

So, a LayoutEntity defines this method:

Properties

A property for the description

Let us look into the property that represents the description. 

Property is the top interface that represents any kind of property. It is generic with two types of parameters. The first represents the class of the owning object and the second is the type of internal values of this property. The internal value is found when getting the value of that property within Jira. It could be a variety of things: a string for properties like a description, a date, a number, or even another object like an issue or a workflow.

Imagine, for example, that a relevant property of your entity is a filter it uses to work only on a specified set of issues; the internal value for that property might be the filter itself. When it is exported, the internal value will be converted into a string (the "external string") and added to the exported contents. The external string must be instance-independent so that its meaning remains constant in any Jira instance. During the import, the reverse conversion will take place from the external string to the internal value.

In the case of the layout description, looking at methods in the Profields Java API, it is obvious that it can be read and set as a java.lang.String. So, this will be the type of internal values for this property. Moreover, the description of the layout is something that should be invariant in any Jira instance where we want to move that layout.

It is possible to code a Property class implementing the methods required by the Property interface. However, in practice, it is easier to use the builders in package com.awnaba.projectconfigurator.extensionpoints.customentities.propertybuilder.  

Property for layout description

Note the following aspects of the code above:

  • The type arguments for the property are Layout (the class of the owning objects) and String (the class for property values). The PropertyBuilder has the same type of arguments.

  • Property has a report name that identifies it to the end user and is also used to organize the exported content in the XML file (something like description, applicable statuses, or default issue type). This property name is passed as an argument to the PropertyBuilder constructor.

  • Given the property name "description", the Builder assumes that the property value is read or set using the Java Beans convention for getters and setters. in this case, that means the following methods in Layout will be used:

Default getters and setters

  • In some cases, after changing the property of an object, additional action must be taken to persist the change. This is required for Layout, as the method LayoutService.update(Layout layout) must be invoked to persist any change to a layout. This is specified in the above code, passing a lambda argument to persistingPostOp().

  • A way to convert from the type of the property internal value to the String that will be exported in the XML and back must be provided. This is specified by the typeValueConverter(TypeValueConverter<W> typeValueConverter) method, where W is the type of the internal value. Several predefined TypeValueConverter instances are provided by the factory com.awnaba.projectconfigurator.extensionpoints.customentities.converters.ConverterFactoryconverts between different classes and String, and from a String to the original value of the class.

  • A Property must also specify if it will be set during object creation. Looking at the method LayoutEntity.createNew(ObjectDescriptor<Layout> objectDescriptor) above, it is clear that the layout description is set when a Layout is created. This is the default assumption used by PropertyBuilder, so no special action is needed in this case. Otherwise, you would invoke setInCreation(false) on the builder.

A property for the associated Profield

ReferenceProperty

Often, a Property of an object consists of a reference to another object or a collection of objects in Jira. These referred objects might be built-in Jira objects (issue types, statuses, filters, etc) or other objects that are supported by a PCJ extension. This is relevant for the migration, so there is a specific sub-interface, ReferenceProperty, that must be implemented for any Property that references other objects.

ReferenceProcessor

Every ReferenceProperty has a ReferenceProcessor. This is the object that is able to convert that reference to an external string and vice versa. It also resolves the reference and handles the cases where the reference is broken (i.e. the referred object is missing in Jira).

How to create a ReferenceProcessor or compose a complex ReferenceProcessor from simpler ones was explained in the workflow and gadget extensions sections. 

Remember that...

... for workflow and gadget extensions, the internal value of the reference was always a String, but in custom entities the internal value can have any type. See the next example. 

In the case of the field item, there is a property to represent its association with a Profield. We are going to use that as an example. Looking at the Profields Java API, you will find that there are methods to get and set the com.deiser.jira.profields.api.field.Field for a field item.

Reference to Profield Property in FieldViewItemEntity.java

There are three things specific to a ReferenceProperty in the above code:

  • It uses a ReferencePropertyBuilder instead of a PropertyBuilder.

  • It calls the referenceProcessor() method to set the ReferenceProcessor associated with this ReferenceProperty. Note that the ReferenceProcessor has been produced by a SimpleReferenceProcessorFactory from the referred entity (the custom entity created for Profields).

  • It is not necessary to set a TypeValueConverter as conversions between the internal type and String are handled by the ReferenceProcessor, including any translation if required.

Other aspects could apply generally to any Property:

  • The referred Profield is part of the cross-instance identifier of a FieldViewWithContainer, and a property that is part of the cross-instance identifier of an object will never be modified for an existing object. This means its setProperty() method will never be invoked. This can be specified to the builder calling the cannotChange(String entityName) method. See the section below about cross-instance identifiers.

  • If the default getter derived from the property name is not appropriate, a different one may be specified using the getter(Function<T, W> getter) method.

Apart from the ReferenceProcessor, a ReferenceProperty is much like any other Property!

Identifiers

An Identifier is, for a given entity, a bidirectional mapping between String(s) and objects of that type. In other words, an Identifier can find the object of that type given a string or find the string that identifies a particular object. For example, the relationship between statuses and their IDs is an Identifier. Given an ID string like "10110", it is possible to find the status with that ID, and from any given status, it is easy to obtain its ID as a string.

There are two flavors of Identifiers. They can be InstanceIndependentIdentifier if the mapping would be the same in any instance of Jira. For example, given the key of a project in Jira, it is possible to find the corresponding project. We expect that the equivalent project in a different Jira instance will have the same key, so this mapping does not depend on a particular Jira instance. On the other hand, we can also map projects to their ID strings (like "10202") and vice versa. However, it is most likely that equivalent projects in different Jira instances will have completely unrelated IDs. Therefore, this mapping changes whenever we look at a different instance. This would be an InstanceSpecificIdentifier. Any Identifier has also a report name like "ID", "name", or "key", typically derived from the property they are based on.

Cross-instance identifiers

Every CustomEntity must have at least one InstanceIndependentIdentifier returned by the method getCrossInstanceIdentifier(). This Identifier will be used to map equivalent objects in different instances.

For example, if this method returns an Identifier based on the object name, PCJ will treat entities of this type with the same name as equivalent objects in different instances. This means that during an import, if PCJ is bringing in an object with the name "X", two situations may occur at the destination instance:

  • No object of the same entity with the name "X" existed previously: then PCJ will create a new object with that name and with the same properties and children it had at the source instance.

  • An object of the same entity with the name "X" existed previously: then PCJ will modify that object so that it has the same properties and children it had at the source instance.

A CustomEntity may have an arbitrary number of additional Identifier(s), either InstanceIndependentIdentifier or InstanceSpecificIdentifier, returned by the methods getOtherInstanceIndependentIdentifiers() and getInstanceSpecificIdentifiers(). Typically, these are used in ReferenceProperty from different entities, for example, when objects of a different entity refer to objects of this entity by their IDs. See this method in SimpleReferenceProcessorFactory:

com.awnaba.projectconfigurator.extensionpoints.extensionservices.SimpleReferenceProcessorFactory

Creating identifiers from properties

Most often, Identifiers are based on one or several properties of an object. In this example, Profields uses a cross-instance identifier based on combining their type and name. There is a service, IdentifierFactory, that facilitates creating Identifiers from object properties:

Creating an Identifier from Two Properties

The method identifierFromProperties() takes, as the first argument, a Function that receives a list with the internal values for the selected properties and returns the object identified by those values. The rest of the arguments are the selected properties. Their order will be the same as that used to build the lists of internal values that will be received by the first Function.

Very often, an identifier will be based on just a single property (like the name of the object). In these situations, you can use a much simpler version of the code above based on this method:

com.awnaba.projectconfigurator.extensionpoints.customentities.IdentifierFactory

For example, a Layout can be identified by its name:

LayoutEntity: creating an identifier by name.


ChildCustomEntity

Some objects in Jira cannot exist outside a larger object (a parent object). A typical example would be the components of a project. No component can exist outside a project. If the enclosing project is removed, then its components will also be removed. If any entity in a PCJ extension exhibits this behaviour relative to another entity, then it should implement the ChildCustomEntity sub-interface.

In the Profields example, there are three entities that clearly meet the conditions to be treated as child entities: section (which cannot exist outside a layout), container (it cannot exist outside a section), and field item (that cannot exist outside its container). Then, their corresponding entity classes will be as in the following:

Entity for Section

Being a ChildCustomEntity has some implications:

  • Note that the interface ChildCustomEntity has two types of arguments: the first is the Java class for the child objects (SectionViewParent in this case), and the second is the Java class for the parent object (Layout).

  • The ChildCustomEntity inherits from its parent CustomEntity the status of configuration or data.

  • Subordinate objects may be removed from the Jira destination instance, giving their parent objects the same set of children as those in the configuration/data being imported.  So, the ChildCustomEntity must implement the method void delete(P parent, T childObject) that removes a given child object.

  • Limitation: both the child and parent entities must belong to the same PCJ extension. Child-parent relationships where one of the entities is in one extension and the other is in a different extension or is a Jira built-in object are not supported.

delete() method

Identifying child objects

There must be a way to identify a child object from a String among the children of a given parent object, which is invariant between different Jira instances. In the example above, the name can be used to identify a component among the components of a known Jira project. This mapping between String and child objects, restricted to a specified parent object, is called a PartialIdentifier. A ChildCustomEntity must have a PartialIdentifier. On the other hand, it does not have to declare a cross-instance identifier, as a default one will be automatically generated from the parent's cross-instance identifier and its PartialIdentifier. As in the case of the identifiers for GlobalCustomEntity, it is easy to create a PartialIdentifier from some properties of the ChildCustomEntity. In the following example, the name of a section is used to identify it within the layout.

Defining a PartialIdentifier for SectionViewEntity

Note that the first argument to method partialIdentifierFromSingleProperty() is a java.util.function.BiFunction that receives as arguments the parent object and the internal value of the selected property for the object to be looked up. Properties that are part of the partial identifier are also indirectly part of the cross-instance identifier. This means that it is not necessary to actually implement updates of those properties.

There is also a similar method in IdentifierFactory to obtain a PartialIdentifier from several properties:

com.awnaba.projectconfigurator.extensionpoints.customentities.IdentifierFactory

A method must be implemented to navigate to the parent object from one of its children: P getParent(T childObject). In this example, given a section, that method must return the layout it belongs to:

Navigate to the Parent Object

Child Collections

Any CustomEntity with children must implement the method Collection<ChildCollection<?,T>> getChildCollections(). Each ChildCollection returned by this method represents a collection of child entities of the same type. This collection has methods to:

  • Identify the ChildCustomEntity of the children: getChildCustomEntity()

  • Find the collection of child objects for a given parent: getSubordinates(P parentObject)

See this example in ContainerViewEntity.java specifying that a container may contain field items:

Create a ChildCollection

Sorting a ChildCollection

Often, the order of some children within the parent object is relevant. In this example, the sections that make up a layout are in a given sequence. If that sequence changed during the migration, end users would see a different layout. If you need to specify that the order of some children is relevant, so that it must be preserved during the migration, follow these steps.

 First, make the child collection implement SortedChildCollection<T, P>, which is a sub-interface of ChildCollection<T, P>.

This sub-interface requires implementing the additional method void sort(List<String> inputList, P parentObject). This method is the one that actually sorts the sections within a layout. Bear in mind that the argument inputList is a list of the partial identifiers of the children in the desired order. It is guaranteed that when this method is called all the children have already been created.

Finally, consider that the original order of the children at the source instance is given by the list returned by method List<T> getSubordinates(P parentObject) in ChildCollection. Make sure that this method returns the children list in the right order!

ExportTriggerProperty

If an app defines new object types, it is likely that these objects will be used somewhere else in Jira. For example, imagine a test management app that creates new entities like test case, test run, or test plan. In this case, a project might use one or more test plans. This implies that, when writing a PC extension for this test management app, we need to specify this use relationship between projects and test plans. This relationship is relevant because we probably want to migrate a project's test plans when the project is migrated. In fact, this is the reason we started writing an extension for PCJ!

In the Profields example, a layout is associated with a project. It seems natural that when a project configuration is moved if that project is using a layout, we will want that layout to be moved too as part of that project configuration. This is achieved with an ExportTriggerProperty. Each instance of this interface represents one type of relationship between an entity not defined in this PCJ extension (like projects) to an entity defined within it (layouts).

LayoutEntity: defining relations to projects.

ExportTriggerProperty is a sub-interface of ReferenceProperty, so the above code using an ExportTriggerPropertyBuilder will already look familiar.

The ExportTriggerProperty must implement two additional methods:

  • String getLinkedEntityName() specifies the "entity name" of the entity (project) related to layouts. Typically, this will be one of the constants defined in class ObjectAlias.

  • Collection<T> getRelatedObjects(L linkedEntity) this method must return the collection of layouts that are linked to a given project.

In the code above that uses the ExportTriggerPropertyBuilder, those two methods are defined by invoking linkedEntityName(ObjectAlias.PROJECT) and relatedObjects(this::getForProject) on the builder. This last method receives a lambda expression, which is a reference to the following method in LayoutEntity:

LayoutEntity: method to find Layout related to a project