How to provide a new complex data type

The use case

The product list data provider needs to provide a new sub-data type that delivers items that are linked to products via a third entity "LinkEntity":

images/download/attachments/45482642/Repo_LinkEntity.png

Implementing the data type

At first, you need to implement a new export data type that provides fields of "Article" and "LinkEntity" entities.

The best way is to subclass CompositeDataType:

public class DataTypeItemsLinkedToProducts extends CompositeDataType
{
public static final String IDENTIFIER = "DataTypeItemsLinkedToProducts"; //$NON-NLS-1$
 
@Override
protected void init()
{
super.init();
 
this.identifier = IDENTIFIER;
this.name = Messages.getString( "DataTypeItemsLinkedToProducts.name" ); //$NON-NLS-1$
this.description = Messages.getString( "DataTypeItemsLinkedToProducts.description" ); //$NON-NLS-1$
this.isListType = true;
this.entityIdentifier = StringUtils.EMPTY;
this.entityName = this.name;
this.entityTypeIdentifier = StringUtils.EMPTY;
this.isInitialized = false;
}
 
@Override
protected void postInitDataTypes()
{
DataTypeUtils.addSubDatatype( this.dataTypes, DataTypeUtils.getSubDatatypePart( "LinkEntity" ) );
DataTypeUtils.addSubDatatype( this.dataTypes, DataTypeUtils.getSubDatatypePart( "Article" ) );
super.postInitDataTypes();
}
}

There're some useful utility methods:

com.heiler.ppm.texttemplate.core.dataprovider.util.DataTypeUtils.addSubDatatype( List subDataTypesList, DataType newDataType );
com.heiler.ppm.texttemplate.core.dataprovider.util.DataTypeUtils.getSubDatatypePart( String entityIdentifier );

Don't forget to contribute the new data type at the com.heiler.ppm.texttemplate.core.dataTypes extension point:

<extension
point="com.heiler.ppm.texttemplate.core.dataTypes">
<dataType
class="com.heiler.ppm.custom.product2g.export.core.datatype.DataTypeItemsLinkedToProducts"
name="ItemsLinkedToProducts">
</dataType>
</extension>

Adding the data type as sub-data type of a data provider

Subclass the standard data provider (not a base impl!), e.g. com.heiler.ppm.product2g.export.core.internal.dataprovider.ProductListDataProvider and overwrite the getSubDataTypes() method.

public class MyProductListDataProvider extends ProductListDataProvider
{
private final static String IDENTIFIER = "MyProductListDataProvider.new"; //$NON-NLS-1$
 
// ...
 
@Override
public List getSubDataTypes()
{
List subDataTypes = super.getSubDataTypes();
// add my own sub-data type
DataTypeUtils.addSubDatatype( subDataTypes, DataTypeItemsLinkedToProducts.IDENTIFIER );
return subDataTypes ;
}
 
// ...
 
}

Consider the utility methods:

com.heiler.ppm.texttemplate.core.dataprovider.util.DataTypeUtils.addSubDatatype( List subDataTypesList, String newDataTypeIdentifier );

Contribute the new data provider at the com.heiler.ppm.texttemplate.core.dataProviders extension point.

<extension
point="com.heiler.ppm.texttemplate.core.dataProviders">
<dataProviders
class="com.heiler.ppm.custom.product2g.export.core.dataprovider.MyProductListDataProvider"
name="ProductList with linked items">
</dataProviders>
</extension>

Extending the data provider to can query data for the new data type

Now you have to implement the algorithm for list model retrieving.

Call your own query method

Ensure your own query method will be called:

public class MyProductListDataProvider extends ProductListDataProvider
{
// ...
 
@Override
public ListModel queryData( DataType dataType, Pair[] fields, ExecutionContext executionContext, int maxCount )
throws CoreException
{
if ( dataType.equals( DataTypeItemsLinkedToProducts.IDENTIFIER ) )
{
return queryLinkedItems( dataType, fields, executionContext );
}
return super.queryData( dataType, fields, executionContext, maxCount );
}
 
 // ...
 
}

The idea

The idea is to get a list model for each entity, and then merge the list models into one resulting list model. It's important that the resulting list model contains all requested fields and a column with the product ids. That id column is used to map the entries of the sub-list model to the entries of the main list model containing products.

The challenge here is to get list models for two further root entities. That's why you cannot use the query and the report result of the product list data provider directly.

The general pattern

Here's the pattern for the queryLinkedItems() implementation:

  1. Get a report of LinkEntity objects that are containing the products to be exported

    1. Use the search framework (or some other mechanism you've implemented already)

    2. Get the search result as ReportResult

  2. Get the LinkEntity list model

    1. Use super.queryData( dataType, fields, executionContext, maxCount );

    2. Switch off list model sorting, storing, mandatory fields checks

  3. Get a report of Article objects that are mapped to the products via the LinkEntity entity

    1. Loop through the LinkEntity list model and collect the ArticleId respectively ArticleProxy values

  4. Get the Article list model

    1. Use super.queryData( dataType, fields, executionContext, maxCount );

    2. Switch off list model sorting, storing, mandatory fields checks

  5. Merge LinkEntity list model and Article list model

    1. Use ListModelMerger

      1. prepare the list models as described in the corresponding merge method: sort by id column, set row identifying column, ...

    2. Do list model sorting, validation, mandatory fields checks, storing

      1. call postPrepareListModel( result, dataType, fields, executionContext );

You'll need helper

Here are some useful helper methods and patterns:

Splitting fields and logical key filter values

You have to ensure the list of requested fields matches the respective entity, that's why you need to split the list of fields and logical key filter values.

useful methods
com.heiler.ppm.texttemplate.core.datatype.CompositeDataType.splitFields( Pair[] fields );
com.heiler.ppm.texttemplate.core.datatype.CompositeDataType.splitLogicalKeyValueFilterMap( Map< String, Object > logicalKeyValueFilterMap );
data provider implementation
protected ListModel queryLinkedItems( DataType dataType, Pair[] fields, ExecutionContext executionContext ) throws CoreException
{
// split fields and additional info
Map< String, Pair[] > splittedFields = ( ( CompositeDataType ) dataType ).splitFields( localFields );
Map< String, Map< String, Object > > splittedAdditionalInfo = ( ( CompositeDataType ) dataType ).splitLogicalKeyValueFilterMap( executionContext.getAdditionalInfoMap() );
 
// query the LinkEntity list model ...
// ... prepare fields and logical key filter values
Entity linkEntity = getLinkEntity();
DataType linkEntityDataType = DataTypeUtils.getSubDatatypePart( linkEntity.getIdentifier );
Pair[] linkEntityFields = splittedFields.get( linkEntity.getIdentifier);
Map< String, Object > linkEntityAddInfo = splittedAdditionalInfo.get( linkEntity.getIdentifier);
 
// ... load the list model
  // ...
 
// query the Article list model...
// ... prepare fields and logical key filter values
 Entity articleEntity = getArticleEntity();
DataType articleEntityDataType = DataTypeUtils.getSubDatatypePart( articleEntity.getIdentifier );
Pair[] articleEntityFields = splittedFields.get( articleEntity.getIdentifier);
Map< String, Object > articleEntityAddInfo = splittedAdditionalInfo.get( articleEntity.getIdentifier);
 
// ... load the list model
// ...
 
// ...
 
}

Add more fields to the fields list

Often, you need to request some more list model columns; or you have to ensure certain columns are in the field's list.

useful methods
com.heiler.ppm.texttemplate.core.dataprovider.util.DataProviderListModelUtils.checkAndAddField( Pair[] fields, String fieldIdentifier );
com.heiler.ppm.texttemplate.core.dataprovider.util.DataProviderListModelUtils.checkAndAddField( Pair[] fields, Field newField );
data provider implementation
protected ListModel queryLinkedItems( DataType dataType, Pair[] fields, ExecutionContext executionContext ) throws CoreException
{
// ...
 
// query the LinkEntity list model ...
// ... prepare fields and logical key value filter valuers
// ...
 
// ... product id and item id is needed
Field productIdField = getRepositoryField( "LinkEntity.Product" );
linkEntityFields = DataProviderListModelUtils.checkAndAddField( linkEntityFields, productIdField );
Field itemIdField = getRepositoryField( "LinkEntity.Item" );
linkEntityFields = DataProviderListModelUtils.checkAndAddField( linkEntityFields, itemIdField );
 
// ...
 
}

Query a list model for a different root entity

The queryDataInternal( ) method uses the query member of the data provider. You need to prepare your data provider instance before calling this method. Afterwards, you should reset that query member.

useful methods
com.heiler.ppm.texttemplate.core.dataprovider.util.DataProviderContextUtils.buildLocalExecutionContext( ExecutionContext executionContext,
boolean storeSublistModel, boolean checkMandatoryFields,
Map< String, Object > additionalInfo )
data provider implementation
protected ListModel queryLinkedItems( DataType dataType, Pair[] fields, ExecutionContext executionContext ) throws CoreException
{
// ...
 
// get the LinkEntity report result
ReportResult linkEntityObjects = getLinkObjects();
 
 // query the LinkEntity list model using the LinkEntity report result
ReportListModelQueryBaseImpl originalQuery = this.query;
AnyListByReportResult tmpQuery = new AnyListByReportResult( linkEntity.getEntityType().getIdentifier(), linkEntityObjects );
this.query = tmpQuery;
// do not sort, do not store the list model
ExecutionContext tmpContext = DataProviderContextUtils.buildLocalExecutionContext( executionContext, false, false, linkEntityAddInfo );
ListModel linkListModel = super.queryDataInternal( linkEntityDataType, linkEntityFields, tmpContext, -1 );
// restore original query
this.query = originalQuery;
 
 // query the Article list model
// ...
// merge both list models
// ...
 
return result;
}

Check the list model's columns, merge list models

If you want to merge list models you have to ensure having that columns in your list model.

Furthermore, the list models have to be sorted by the columns used as merge criteria.

useful methods
com.heiler.ppm.texttemplate.core.dataprovider.util.DataProviderListModelUtils.getColumnIndexByFieldId( ListModel listModel, String fieldId );
com.heiler.ppm.texttemplate.core.dataprovider.util.DataProviderListModelUtils.sortListModel( ListModel listModel, int col1Index, int col2Index );
com.heiler.ppm.texttemplate.core.dataprovider.util.DataProviderListModelUtils.setListModelRowIdentifyingColumn( ListModel listModel, int columnIndex );
 
com.heiler.ppm.std.core.list.merger.ListModelMerger.merge*;
data provider implementation
protected ListModel queryLinkedItems( DataType dataType, Pair[] fields, ExecutionContext executionContext ) throws CoreException
{
// ...
 
// query the LinkEntity list model
ListModel linkListModel = ...;
 
 // query the Article list model
ListModel itemListModel = ...;
 // merge both list models by the item id column
int linkItemIdColumnIndex = DataProviderListModelUtils.getColumnIndexByFieldId( linkListModel, itemIdField );
// list models of root entities always contain a column for the identifying field type
int itemIdColumnIndex = DataProviderListModelUtils.getColumnIndexByFieldTypeId( itemListModel, "ArticleType.Id" )
 
DataProviderListModelUtils.sortListModel( linkListModel, linkItemIdColumnIndex, -1 );
DataProviderListModelUtils.sortListModel( itemListModel, itemIdColumnIndex , -1 );
 
ListModelMerger listModelMerger = new ListModelMerger();
listModelMerger.mergeNxM( linkListModel, itemListModel, linkItemIdColumnIndex, itemIdColumnIndex );
 
// ...
}

Store the resulting sub-list model

If a sub-list model has to be stored, it's grouped by the "master ids". That's why each sub-list model needs a column that can be used for this. Usually, the identifier column has the identifying field path of the data provider's root entity. If this is not so, you need to take care of it yourself.

data provider implementation
protected ListModel queryLinkedItems( DataType dataType, Pair[] fields, ExecutionContext executionContext ) throws CoreException
{
ListModel result = null;
 
// query the LinkEntity list model
// ...
// query the Article list model
// ...
// merge both list models
// ...
 
// product proxy is identifying field to get sub-entries from this list model
this.identifyingFieldIdentifier = productIdField.getInitialIdentifier();
 
// sort, validate, store the listmodel now
postPrepareListModel( result, dataType, fields, executionContext );
return result;
}