Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

  • 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.

...

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:

...

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.

...

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.

...

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.

...

Code Block
languagejava
@Override
public Collection<Property<Layout,?>> getProperties() {
	return Arrays.asList(nameProperty, descriptionProperty, layoutProjectsProperty);
}

A property for the description

Let us look into the property that represents the description. 

...

  • 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).

...

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.

...

Code Block
languagejava
<T> ReferenceProcessor<String> getFromNewObjectType(CustomEntity<T> customEntity, Identifier<T> internalRepresentation);

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:

...

Code Block
languagejava
InstanceIndependentIdentifier<Layout> idByName = identifierFactory.identifierFromSingleProperty(
				layoutService::getByName, nameProperty
        );


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.

...

Code Block
languagejava
@Override
    public void delete(Layout layout, SectionViewParent sectionViewParent) {
        updater.remove(
                layout, sectionViewParent,
                (sectionViewP, sectionViewBuilderList) -> sectionViewP.findMyBuilderIn(sectionViewBuilderList),
                (sectionViewP, sectionViewBuilderList) -> sectionViewBuilderList,
                (sectionViewBuilder, sectionViewBuilderList) -> sectionViewBuilderList.remove(sectionViewBuilder)
        );
    }

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.

...

Code Block
languagejava
<T, P> PartialIdentifier<T, P> partialIdentifierFromProperties(BiFunction<P, List<Object>, T> finder, Property<T, ?>... properties);

Navigate to the parent

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:

...

Code Block
languagejava
@Override
public Layout getParent(SectionViewParent sectionViewParent) { 
	return sectionViewParent.getParent();
}

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:

...

Tip

As shown in this example, a ChildCustomEntity can have its own children!

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.

...

Code Block
languagejava
@Override
    public List<SectionViewParent> getSubordinates(Layout layout) {
        return layout.getSections().stream().
                map(section -> new SectionViewParent(section, layout, layoutService)).
                collect(Collectors.toList());
    }

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!

...