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 to 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 fields items inside a container. We expect, however, that the part of the example that manages ordering of sections will make it easier to implement a similar solution for containers or field items.

Example source code

The complete source code for this example is available at https://bitbucket.org/Adaptavist/example-custom-entities-extension/src/master.

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 represent a reference to other entities?
  5. A Jira admin using PC 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 a 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 take 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 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 PC extension. If objects of a given type can exist on their own, then they are instances of a GlobalCustomEntityIn 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> {
....

JAVA

Entity for Profields

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


JAVA

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 PC 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 PC 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);
	}
JAVA

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 to 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. PC 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).

NOTE: 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 treatment 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 to a Layout:

  • Name
  • Description
  • The project it is associated with

So, a LayoutEntity defines this method:

Properties

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

JAVA

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, 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

Property<Layout, String> descriptionProperty = new PropertyBuilder<Layout,String>("description").
				typeValueConverter(converterFactory.forClass(String.class)).
				persistingPostOp(layoutService::update).build();
JAVA

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

String getDescription();
void setDescription(String description);
JAVA
  • 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.ConverterFactory, that convert 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 which are supported by a PC 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

ReferenceProperty<FieldViewWithContainer, Field> profieldsRefProperty = new ReferencePropertyBuilder<FieldViewWithContainer, Field>("Profield").
				cannotChange(getTypeName()).
				referenceProcessor(simpleReferenceProcessorFactory.getFromNewObjectType(profieldEntity)).
				getter(fvc -> fvc.getFieldView().getField()).
				build();
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 to 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 which 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 which 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 flavours 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 InstanceSpecificIdentifierAny 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, PC will treat entities of this type with the same name as equivalent objects in different instances. This means that during an import, if PC is bringing in an object with name "X", two situations may occur at the destination instance:

  • No object of the same entity with name "X" exists previously: then PC 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 name "X" exists previously: then PC 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

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

JAVA

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

	private InstanceIndependentIdentifier<Field> buildNameTypeIdentifier(){
		return identifierFactory.identifierFromProperties(
				list -> findFieldByNameAndType(list.get(0).toString(), (FieldType)list.get(1)),
		nameProperty, fieldTypeProperty);
	}

	private Field findFieldByNameAndType(String name, FieldType type){
		List<Field> fields = fieldService.get(name);
		fields.removeIf(field -> !field.getType().equals(type));
		return fields.isEmpty() ? null : fields.get(0);
	}

JAVA

The method identifierFromProperties() takes, as first argument, a Function which receives a list with the internal values for the selected properties and returns the object identified by those values. The rest of 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

<T, W> InstanceIndependentIdentifier<T> identifierFromSingleProperty(Function<W, T> finder, Property<T, W> property);
JAVA

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

LayoutEntity: creating an identifier by name.

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


ChildCustomEntity

Some objects in Jira cannot exist outside a larger object (a parent object). A typical example would be the components in 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 PC extension exhibits this behaviour relative to other entity, then it should implement the ChildCustomEntity sub-interface.

Objects in the Profields API that are part of a Layout (like SectionView, ContainerView and FieldView) do not have methods to navigate up the hierarchy and find their parent objects. In order to support this navigation and other issues, we have defined “extended” entities that combine each of those items with a reference to the owning parent for each of them. The extended entities are SectionViewParent, ContainerViewParent and FieldViewWithContainer.

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

@Profile("pc4j-extensions")
@Component
public class SectionViewEntity implements ChildCustomEntity<SectionViewParent, Layout> {
…

JAVA

Being a ChildCustomEntity has some implications:

  • Note that interface ChildCustomEntity has two type arguments: the first is the Java class for the child objects (SectionViewParent in this case), 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 PC 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

@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)
        );
    }
JAVA

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

    private PartialIdentifier<SectionViewParent, Layout> buildPartialIdByName(){
		return identifierFactory.partialIdentifierFromSingleProperty(
				(Layout layout, String name) -> layout.getSections().stream().
							filter(section -> section.getName().equals(name)).
							map(sectionView -> new SectionViewParent(sectionView, layout, layoutService)).
							findFirst().orElse(null),
				nameProperty);
	}
JAVA

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 update of those properties.

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

com.awnaba.projectconfigurator.extensionpoints.customentities.IdentifierFactory

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


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:

Navigate to the Parent Object

@Override
public Layout getParent(SectionViewParent sectionViewParent) { 
	return sectionViewParent.getParent();
}
JAVA

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

private ChildCollection<FieldViewWithContainer, ContainerViewParent> buildFieldViewItemChildren(){

        return new ChildCollection<FieldViewWithContainer, ContainerViewParent>(){
            @Override
            public ChildCustomEntity<FieldViewWithContainer, ContainerViewParent> getChildCustomEntity() {
                return fieldViewItemEntity;
            }

            @Override
            public List<FieldViewWithContainer> getSubordinates(ContainerViewParent containerViewParent) {
                return getFieldViewItems(containerViewParent);
            }
        };
    }

JAVA

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.

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

@Profile("pc4j-extensions")
@Component
public class SortedSectionChildCollection implements SortedChildCollection<SectionViewParent, Layout> {
…

JAVA

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.

@Override
    public void sort(List<String> inputList, Layout layout) {
        updater.update(layout, null,
                (ignoredItem, builderList) -> builderList,
                builderList -> sortAs(builderList, inputList));
    }

    private void sortAs(List<SectionViewBuilder> builderList, List<String> inputList) {
        builderList.sort(Comparator.comparingInt(o -> inputList.indexOf(o.getName())));
    }

JAVA

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!

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

JAVA

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 PC!

In the Profields example, a layout is associated to 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 PC extension (like projects) to an entity defined within it (layouts).

LayoutEntity: defining relations to projects.

		ExportTriggerProperty<Layout, Project, Project> layoutProjectsProperty = new ExportTriggerPropertyBuilder<Layout, Project, Project>("project").
				setInCreation(false).getter(this::getProject).
				setter((layout, project) -> layoutService.associateToProject(layout, project.getId())).
				referenceProcessor(processorFactory.getJiraObjectReferenceProcessor(Project.class)).
				linkedEntityName(ObjectAlias.PROJECT).
				relatedObjects(this::getForProject).
				build();
JAVA

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 invoking linkedEntityName(ObjectAlias.PROJECT) and relatedObjects(this::getForProject) on the builder. This last method receives a lambda expression, that is a reference to the following method in LayoutEntity:

LayoutEntity: method to find Layout related to a project

	private Collection<Layout> getForProject(Project p) {
		Layout found = layoutService.getByProject(p);
		return found==null ? Collections.emptyList() : Collections.singletonList(found);
	}
JAVA

Remember...

Functionally speaking, PC allows the migration of projects with all their configuration or data. This means that PC, when asked to export a group of projects, will include in the export file those projects plus all the objects that are directly or indirectly referenced by them. For example, a project might use a workflow scheme (direct reference) and the workflow scheme might use a workflow that in turn references a custom field (indirect references). All three (the workflow scheme, the workflow, and the custom field) will be exported alongside the project. On the other hand, a workflow that is not used by any of those projects will not be exported.

The same applies to the objects defined by PC extensions. If they are directly or indirectly part of the project configuration/data, they will be exported, otherwise they will be ignored. This means that, in order to be included in the migration, your app objects can be related directly to the project (as in the example) or indirectly through another object that is referenced by the project (like a custom field or a workflow which are used by that project).

This is the default process implemented by PC. However, there are situations when you would like to export all entities of some kind, no matter what explicit relation they may have to the exported projects. This typically happens with entities that represent globally available objects.

In this case, make this entity implement NonProjectConfigCustomEntity<T>, which is a sub-interface of GlobalCustomEntity<T>. This means your class will have to implement method Collection<T> findAll(Set<String> exportedProjectKeys). This method must return the collection of objects of this type that must be exported. Note that the set of keys of the projects being exported is supplied, however this argument can be ignored (and we expect this to happen quite often!).