Versions Compared

Key

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

On this page:

Table of Contents
minLevel1
maxLevel6
outlinefalse
styledefault
typelist
printabletrue

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

...

Example source code

...

  • .

...

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.

...

  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 represents a reference to other entities?

  5. A Jira admin using PC 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?

...

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:

...

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:

...

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

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


Entity for Profields

Code Block
languagejava
@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 Custom Entity 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 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 PC PCJ extension.

Creating new objects

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

Creating New Layouts

Code Block
languagejava
@Override
public Layout createNew(ObjectDescriptor objectDescriptor) { 
	Map<String,Object> properties = objectDescriptor.getProperties(); 
	LayoutBuilder builder = layoutService.getLayoutBuilder(); 
	builder.setName((String)properties.get(nameProperty.getPropertyName()));
	builder.setDescription((String)properties.get(descriptionProperty.getPropertyName())); 
	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 has a map that contains internal values for all its properties keyed by the property names.

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 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. PC 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 to with a Layout:

  • Name

  • Description

  • The project it is associated to

So, a LayoutEntity defines this method:

Properties

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. 

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 instance of Jira where we want to move that layout. For simple properties that have strings as internal values, whose content is invariant across different instances of Jira and do not contain a reference to other Jira/app objects, there is a specific subinterface of Property called StringProperty:

Property for Name

Code Block
languagejava
private StringProperty<Layout> buildDescriptionProperty(){

	return new StringProperty<Layout>() 
		{ @Override
		public String getInternalValue(Layout layout) {
			// return null means "this property is empty"
			return layout.getDescription();
		}

		@Override
		public void setProperty(Layout layout, String s)
	 		{ layout.setDescription(s); 
	 		layoutService.update(layout);
		}

		@Override
		public String getPropertyName() { 
			return "description";
		}

		@Override
		public boolean isSetInCreation() { 
			return true;
		}
	};
}

Additionally, a Property has a report name that identifies it to the end user (something like description, applicable statuses, or default issue type). See the above method getPropertyName().

There is also a method to set the property on a given object (see setProperty() method). This takes as arguments, the layout that will be modified and the new internal value of the description. A Property also implements the method boolean isSetInCreation() that returns whether or not this property is set during object creation, as specified by its owning CustomEntity createNew() method. If this method returns true, PC PCJ will know that it does not have to set this property after creating a new object. In this case, the method to create a layout (as seen before) sets its name and description, so the isSetInCreation() method for the description must return true.

A property for the associated Profield

ReferenceProperty

Often, a Property property of an object consists of a reference to another object or to a collection of objects in Jira. These referred objects might be built-in Jira objects (issue types, statuses, filters, etc.) or other objects which that are supported by a PC PCJ extension. This is relevant for the migration, so there is a specific sub-interface, ReferenceProperty, that must be implemented for any Property 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 does not exist in Jira). ReferenceProcessor(s) will be supplied by some of the factory classes mentioned above. You should not create your own ReferenceProcessor(s) , except as a composition of ReferenceProcessor(s) provided by PC PCJ factory classes.

In the next section, we will review different types of ReferenceProperty(s)and their associated ReferenceProcessor(s):

...

A ReferenceProperty whose internal value is a string. It may need translation to be converted into an external string or not. For example, if it contains the ID string of the referred object, e.g., "11100", then it has to be translated during the migration to a different ID. The internal string may contain references to one or several objects.

As shown in the section about workflow and gadget extensions, the integration framework facilitates composing a ParamValueTranslator to handle quite complex strings , from one or several simpler ParamValueTranslator(s), so this is the recommended approach if you have to handle complex properties, for example, like a JSON string that may contain ids of several entities (like custom fields and options). 

...

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. Additionally, a field item cannot refer to more than one Profield, so it fits into an ExtendedObjectProperty:

Reference to Profield Property in FieldViewItemEntity.java

Code Block
languagejava
private ExtendedObjectProperty<FieldViewWithContainer, Field> buildProfieldsRefProperty(){
        return new ExtendedObjectProperty<FieldViewWithContainer, Field>(){
            @Override
            public ObjectReferenceProcessor<Field> getReferenceProcessor() {
                // This is an ObjectReferenceProcessor to one of the entities defined in this app
                return objectReferenceProcessorFactory.getObjectReferenceProcessor(profieldEntity);
            }

            @Override
            public Field getInternalValue(FieldViewWithContainer fieldViewWithContainer) {
                return fieldViewWithContainer.getFieldView().getField();
            }

            @Override
            public void setProperty(FieldViewWithContainer fieldViewWithContainer, Field field) {
                throw new IllegalStateException("The field of an existing FieldView should never be modified");
            }

            @Override
            public String getPropertyName() {
                return "Profield";
            }

            @Override
            public boolean isSetInCreation() {
                return true;
            }
        };
    }

Info

You will notice the setProperty() method has not been implemented in this case, instead it throws an exception if it is invoked. The reason is that this property is part of the identifier of a FieldViewWithContainer, and a property

...

that is part of the identifier of an object will never be modified for an existing object. This means its setProperty() method will never be invoked.

See the section below about cross-instance identifiers.

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 finding find the string which 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 flavours 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, 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 PCJ will treat entities of this type with the same name as equivalent objects in different instances. This means that during an import, if PC 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" exists existed previously: then PC 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" exists existed previously: then PC 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 as, for example, when objects of a different entity refer to objects of this entity by their IDs. See methods such as this in TranslatorFactory:

com.awnaba.projectconfigurator.extensionpoints.extensionservices.TranslatorFactory

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

Creating identifiers from properties

Most often, Identifiers will be based on one or several properties of an object. In this example, Profields will use 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

Code Block
languagejava
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);
}

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

Infotip

If a property is part of the cross-instance identifier for an entity, it is not necessary that its setProperty(...) method actually updates the property, as it will never be called.

ChildCustomEntity

Some objects in Jira cannot exist outside a larger object (a parent object). A typical example would be the components in of a project. No component can exist outside of a project. If the enclosing project is removed, then its components will also be removed. If any entity in a PC PCJ extension exhibits this behaviour relative to other another 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

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

Being a ChildCustomEntity has some implications:

  • The ChildCustomEntity will inherit from its parent, CustomEntity, the status of being 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.

delete() method

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

There must be a way to identify a child object from a String among the children of a given parent object. 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

Code Block
languagejava
    private PartialIdentifier<SectionViewParent, Layout> buildPartialIdByName(){
        return identifierFactory.partialIdentifierFromProperties(
                (Layout layout, List<Object> singletonList) -> {
                    String name = (String)singletonList.get(0);
                    return layout.getSections().stream().
                            filter(section -> section.getName().equals(name)).
                            map(sectionView -> new SectionViewParent(sectionView, layout, layoutService)).
                            findFirst().orElse(null);
                },
                nameProperty);
    }

Note that the first argument to method partialIdentifierFromProperties() is a java.util.function.BiFunction that receives as arguments the parent object and the list of internal values of the selected properties for the object to be looked up. Properties that are part of the partial identifier also are indirectly part of the cross-instance identifier, this means that it is not necessary to actually implement update of those properties in their setProperty(…) method.

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

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:

...

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

Create a ChildCollection

Code Block
languagejava
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);
            }
        };
    }

Infotip

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

...

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, the 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 child collection implement SortedChildCollection<T, P>, which is a sub-interface of ChildCollection<T, P>.

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

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.

Code Block
languagejava
@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())));
    }

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!

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 PCJ 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 PCPCJ!

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

ExportTriggerProperty is a sub-interface of ReferenceProperty, so it will implement methods like:

LayoutProjectsProperty

Code Block
languagejava
public class LayoutProjectsProperty implements ExportTriggerProperty<Layout, Project, Project> {
...
	@Override
	public ReferenceProcessor<Project> getReferenceProcessor() {
		return objectReferenceProcessorFactory.getObjectReferenceProcessor(Project.class);
	}
...

And it 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.

LayoutProjectsProperty

Code Block
languagejava
public class LayoutProjectsProperty implements ExportTriggerProperty<Layout, Project, Project> {
...
	@Override
	public String getLinkedEntityName() { 
		return ObjectAlias.PROJECT;
	}

	@Override
	public Collection<Layout> getRelatedObjects(Project project) {
		return Collections.singletonList(layoutService.getByProject(project));
	}
...

...

Infotip

Remember...

Functionally speaking, PC PCJ allows the migration of projects with all their configuration or data. This means that PC, when PCJ is asked to export a group of projects, it will include in the export file those projects plus all the objects that are directly or indirectly referenced by them in the export file. 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 PCJ 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 that are used by that project).

This is the default process implemented by PCPCJ. 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 the 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!).