Skip to content

Context composition and decomposition

julia-fms edited this page Nov 29, 2019 · 68 revisions

Context composition and decomposition

Contents

  1. Context as execution environment
  2. Simple functional actions on centres
  3. Simple functional actions on masters
  4. Drill-down details (embedded centres with query enhancers)

Context as execution environment

Context represents environment in which action executes. Let's take a look at some simplistic examples of where environment could be helpful and where it does not matter.

Consider adding new action to the master for entity Asset (number AA0116634111) with properties Parent (AADME) and Peer (AA012A72558). Let's define simple action on Asset master that will show all related assets. We can relate assets through Asset's Parent. This means that we will show all assets that have the same Parent as the Asset on which the action has been performed (in our case AADME). In this case the context would be represented as a pair of Asset AA0116634111 (master entity) itself and its Parent AADME property (chosen property). Also we can relate assets through Asset's Peer AA012A72558 using the same action and in this case the context will consist of Asset AA0116634111 itself and its Peer AA012A72558 property.

But what about the action that creates child asset? This action should open Asset master to create new Asset and pre-populate Parent property with the value AA0116634111. The context of such action will be represented as the Asset AA0116634111 (master entity) from which child asset should be created.

When creating asset that does not depend on any other asset the top Add New action in Asset centre should be used. This action does not require any context, it will be empty in this case.

In cases where actions are located on the Asset centre's EGI we might need to know selected EGI assets to perform action against them. However, if no instances are selected, we might use selection criteria parameters to perform action against every instance matching selection criteria. The action to dispose assets can be an example for this. Here both selected entities and selection criteria will be needed to compose the context of the action.

These trivial examples show that action execution depends on some environment around it and we name it execution context. It is very important to understand how it can be defined and used properly. Let's start from simplest centre actions requiring context.


Simple functional actions on centre

When adding new action to the centre the following should be performed:

  • context configuration should be imported simply by using EntityCentreContextSelector.context static import
  • EntityActionConfig has to be created and added to the centre configuration
...
import static ua.com.fielden.platform.web.centre.api.actions.impl.EntityActionBuilder.action;
import static ua.com.fielden.platform.web.centre.api.context.impl.EntityCentreContextSelector.context;

...
final EntityActionConfig disposeAssetAction = action(DisposeAssetAction.class)
        .withContext(context().withSelectionCrit().withSelectedEntities().build())
        . ...
        .build();

...
final EntityCentreConfig<Asset> centreConfig = EntityCentreBuilder.centreFor(Asset.class)
        ...
        .addTopAction(disposeAssetAction).also()
        ...

Composing of context for configuration is done using chaining of .with...() calls that can add master entity, selected entities / current entity, selection criteria, chosen property and / or computation. Note that we only add the necessary parts. All parts that are not applicable (e.g. selection criteria on master's actions) or just not required should not be added to the context configuration. This is done to reduce the data transmitted / marshalled during action execution. In the end context configuration should be concluded with .build() call.

DisposeAssetAction requires both selected entities and selection criteria of the centre. The action should dispose all selected Assets in EGI in case some where marked by user. Otherwise all assets matching selection criteria will be disposed by the action. However, we give user the choice of whether Dispose selected? or Dispose all?, but automatically suggest Dispose selected? if there are at least one selected entity. Let's start by differentiating both situations in functional action definition.

/**
 * Action to dispose many assets at once.
 */
@EntityTitle("Dispose Assets")
@KeyType(NoKey.class)
@CompanionObject(IDisposeAssetAction.class)
public class DisposeAssetAction extends AbstractFunctionalEntityWithCentreContext<NoKey> {

    @IsProperty
    @Title(value = "Dispose all?", desc = "Dispose all assets that match the selection criteria and appear in the centre?")
    // warn user if too many assets is about to be disposing:
    @BeforeChange(@Handler(DisposeAssetActionDisposeAllValidator.class))
    // toggle 'disposeSelected' after toggling 'disposeAll':
    @AfterChange(DisposeAssetActionDisposeAllSelectedDefiner.class)
    private boolean disposeAll = false;

    @IsProperty
    @Title(value = "Dispose selected?", desc = "Dispose only explicitly selected assets?")
    // prohibit disposing of selected assets if there are no selected entities in EGI:
    @BeforeChange(@Handler(DisposeAssetActionDisposeSelectedValidator.class))
    // toggle 'disposeAll' after toggling 'disposeSelected':
    @AfterChange(DisposeAssetActionDisposeAllSelectedDefiner.class)
    private boolean disposeSelected = false;

    @IsProperty(Long.class)
    @Title(value = "Selected Entity IDs", desc = "IDs of selected in EGI entities")
    private final Set<Long> selectedEntityIds = new HashSet<>();

    @IsProperty
    @Title(value = "Context Holder", desc = "Object for selection criteria entity restoration to run centre query")
    private CentreContextHolder contextHolder;
    ...

Functional entity is produced with the access to execution context. This is the first stage of entity lifecycle, which will concludes with entity master appearing to user. We shall present the master with two mutually exclusive checkboxes Dispose selected? and Dispose all?. How do we define which checkbox to be selected by default?

/**
 * A producer for new instances of entity {@link DisposeAssetAction}.
 */
public class DisposeAssetActionProducer extends DefaultEntityProducerWithContext<DisposeAssetAction> {
    @Inject
    public DisposeAssetActionProducer(final EntityFactory factory, final ICompanionObjectFinder coFinder) {
        super(factory, DisposeAssetAction.class, coFinder);
    }

    @Override
    protected DisposeAssetAction provideDefaultValues(final DisposeAssetAction entity) {
        if (contextNotEmpty()) {            
            // take selectionCrit's context holder and initialise functional entity's corresponding property
            entity.setContextHolder(selectionCrit().centreContextHolder());
            if (selectedEntitiesNotEmpty()) {
                // store IDs of selected entities if there are any
                entity.setSelectedEntityIds(selectedEntityIds());
                // check `Dispose selected?` checkbox by default
                entity.setDisposeSelected(true);
            } else {
                // check `Dispose All?` checkbox by default
                entity.setDisposeAll(true);
            }
        }
        // the context is not present only if Cancel button is being tapped on action master -- no properties should be initialised
        return entity;
    }
}

We provide initial values for functional entity properties inside producer. Please note that producer is used not only when entity master opens, but also when Cancel button is being tapped - this usually leads to functional entity master closing. In this case the context will be empty and there is no need to initialise any properties.

We know that selection criteria and selected entities are included into context configuration. If selected entities are not empty, Dispose selected? checkbox gets selected and IDs get captured into functional entity. Otherwise Dispose all? checkbox gets selected. This choice however provides only a suggestion and can be changed by the user. For example, if there is at least one selected entity the user still can select Dispose all? checkbox to run centre query and dispose all matching assets. That is why we set context holder (entity.setContextHolder(selectionCrit().centreContextHolder());) to be able to restore selection criteria entity and run centre's query in companion's save method.

/**
 * DAO implementation for companion object {@link IDisposeAssetAction}.
 */
@EntityType(DisposeAssetAction.class)
public class DisposeAssetActionDao extends CommonEntityDao<DisposeAssetAction> implements IDisposeAssetAction {
    private final ICriteriaEntityRestorer criteriaEntityRestorer;

    @Inject
    public DisposeAssetActionDao(final IFilter filter, final ICriteriaEntityRestorer criteriaEntityRestorer) {
        super(filter);
        this.criteriaEntityRestorer = criteriaEntityRestorer;
    }

    @Override
    @SessionRequired
    public DisposeAssetAction save(final DisposeAssetAction entity) {
        final EntityResultQueryModel<Asset> query;
        if (entity.isDisposeAll()) {
            query = select(Asset.class).where().prop("id").in() // query based on selection crit
                    .model(
                        criteriaEntityRestorer.restoreCriteriaEntity(entity.getContextHolder())
                        .createQuery().model()
                    ).model();
        } else {
            query = select(Asset.class).where().prop("id").in() // query based on selected entities
                    .values(
                        entity.getSelectedEntityIds().toArray()
                    ).model();
        }

        final fetch<Asset> assetFetch = co$(Asset.class).getFetchProvider().fetchModel(); // fetch model 
        final AssetStatus statusX = co(AssetStatus.class).findByKey("X"); // 'disposed status'
        try (final Stream<Asset> stream = co$(Asset.class).stream(from(query).with(assetFetch).model())) {
            stream.forEach(asset -> { // stream assets not to blow up heap for large numbers of entities
                asset.setStatus(statusX);
                if (asset.isValid().isSuccessful()) {
                    co$(Asset.class).save(asset);
                }
            });
        }
        return super.save(entity);
   }

The pattern is simple - as first step in producer initialise values of functional entity from its execution context, then use captured values inside companion object's save method.


Simple functional actions on masters

Consider another example of action that will populate description of Asset Certification entity based on related Asset entity. This will provide ability to auto-populate description for Asset Certification in case no specific description to be provided.

Every Asset Certification relates to some specific Asset for which the certification is required. This is directly expressed by AssetCertification entity having Asset-typed property as its composite key member:

@DescTitle("Description")
public class AssetCertification extends AbstractPersistentEntity<DynamicEntityKey> implements IAssetCertification {
    @IsProperty
    @Title("Asset")
    @MapTo
    @CompositeKeyMember(1)
    @Readonly
    @Final
    private Asset asset;
    
    ...

Lets define an property action that will be located on Asset Certification master near property Description like this:

...
final EntityActionConfig importDescAction = action(ImportDescAction.class)
        .withContext(context().withMasterEntity().build())
        .postActionSuccess(() -> new JsCode("self.setEditorValue4Property('desc', functionalEntity, 'desc');"))
        . ...
        .build();

...
final IMaster<AssetCertification> masterConfig = new SimpleMasterBuilder<AssetCertification>().forEntity(AssetCertification.class)
        ...
        .addProp("desc").asMultilineText().withAction(importDescAction).also()
        ...

Note that master entity is included into the context configuration. This represents the master entity on which the action is placed (and actions upon) and in our case it is represented by Asset Certification entity. The action is provided into Asset Certification master configuration by means of associating it to Description (desc) property. If we have Asset Certification then we can use its Asset property to pre-populate description.

It's worthwhile to explain the post-action: self.setEditorValue4Property('desc', functionalEntity, 'desc');. Basically it sets the value for desc property editor of Asset Certification master with the value from desc property of ImportDescAction functional entity (without saving).

Let's define ImportDescAction functional entity with Description (desc) property that will store retrieved Asset's description.

/**
 * Action entity used to populate description to AssetCertification.desc.
 */
@KeyType(NoKey.class)
@CompanionObject(IImportDescAction.class)
@DescTitle(value = "Description", desc = "Collected Desc from related Asset")
public class ImportDescAction extends AbstractFunctionalEntityWithCentreContext<NoKey> {
    ...

Do we expect any input from the user when pre-populating description from asset during action execution? In other words, is there a need to open special action master and directly ask user for the Description? Fortunately, no - we can design the action just to replace the contents of AssetCertification.desc property by new value. After the action is executed AssetCertification.desc property can be directly modified by user.

In this case we will use No-UI Action pattern. We will build No-UI Action master in a following way:

import static ua.com.fielden.platform.web.view.master.EntityMaster.noUiFunctionalMaster;
...

    /**
     * Creates no-UI master for {@link ImportDescAction}.
     */
    private EntityMaster<ImportDescAction> createMaster(final Injector injector) {
        return noUiFunctionalMaster(ImportDescAction.class, ImportDescActionProducer.class, injector);
    }

And then producer will implement all necessary action logic by simply coping Asset's Description in to the AssetCertification.desc:

public class ImportDescActionProducer extends DefaultEntityProducerWithContext<ImportDescAction> {
    ... /* standard constructor here */

    @Override
    protected ImportDescAction provideDefaultValues(final ImportDescAction entity) {
        if (contextNotEmpty()) {
            final AssetCertification masterEntity = masterEntity(AssetCertification.class); // we know exact type here
            entity.setDesc(masterEntity.getAsset().getDesc());
        }
        return super.provideDefaultValues(entity);
    }
}

... and companion object can be empty:

@EntityType(ImportDescAction.class)
public class ImportDescActionDao extends CommonEntityDao<ImportDescAction> implements IImportDescAction {
    @Inject
    public ImportDescActionDao(final IFilter filter) {
        super(filter);
    }
}

We have used master entity part of the context for the action defined on entity master. This essential part can be combined with chosen property or computation. No other parts are applicable when adding action to the master.


Drill-down details (embedded centres with query enhancers)

Master entity part of the context has been used in previous section to demonstrate how to build contextual dependent action on entity master. Let's now consider embedded centre with results that depend on some intrinsic conditions outside of selection criteria scope.

For example we can show Asset Details for some concrete Asset instance or its related Assets based on some property (e.g. Parent). These situations are modelled through embedded centre pattern. Let's define simple action and associate it with four properties on Asset master.

...
final EntityActionConfig displayRelatedOpenAssets = action(OpenRelatedAssetsAction.class)
        .withContext(context().withMasterEntity().build())
        . ...
        .build();

...
final IMaster<Asset> masterConfig = new SimpleMasterBuilder<Asset>().forEntity(Asset.class)
        ...
        .addProp("parent").asAutocompleter()
                .withAction(displayRelatedOpenAssets).also() 
        .addProp("peer").asAutocompleter()
                .withAction(displayRelatedOpenAssets).also()
        .addProp("capitalProject").asAutocompleter()
                .withAction(displayRelatedOpenAssets).also()
        ...

The action will be empty as well as its companion. Please note that producer will be empty too, so there is no need to create it at all.

/**
 * Action entity used to show list of all related open assets with the same Parent, Peer or Capital Project as current Asset.
 */
@KeyType(NoKey.class)
@CompanionObject(IOpenRelatedAssetsAction.class)
public class OpenRelatedAssetsAction extends AbstractFunctionalEntityWithCentreContext<NoKey> {
}
@EntityType(OpenRelatedAssetsAction.class)
public class OpenRelatedAssetsActionDao extends CommonEntityDao<OpenRelatedAssetsAction> implements IOpenRelatedAssetsAction {
    @Inject
    public OpenRelatedAssetsActionDao(final IFilter filter) {
        super(filter);
    }
}

So then we define Action master with embedded centre in a following way ...

import ua.com.fielden.platform.web.view.master.api.with_centre.impl.MasterWithCentreBuilder;
...

    /**
     * Creates master with embedded centre for {@link OpenRelatedAssetsAction}.
     */
    private EntityMaster<OpenRelatedAssetsAction> createMaster(final Injector injector) {
        return new EntityMaster<OpenRelatedAssetsAction>(
            OpenRelatedAssetsAction.class,
            new MasterWithCentreBuilder<OpenRelatedAssetsAction>()
                .forEntityWithSaveOnActivate(OpenRelatedAssetsAction.class)
                .withCentre(createRelatedOpenAssetsCentre()).done(),
            injector
        );
    }

... where createRelatedOpenAssetsCentre is defined like this:

    private EntityCentre<Asset> createRelatedOpenAssetsCentre() {
        ...
        final EntityCentreConfig<Asset> centreConfig = EntityCentreBuilder.centreFor(Asset.class)
            .runAutomatically() // is needed to make the centre properly autorunnable
            . ...
            .setQueryEnhancer(RelatedOpenAssetsCentreQueryEnhancer.class, context().withMasterEntity().build())
            .build();
        return new EntityCentre<>(MiRelatedOpenAssets.class, "MiRelatedOpenAssets", centreConfig, injector, null);
    }

And now we have query enhancer to define how the centre relates to its context. Note that we include master entity part for query enhancer context. What entity would it be? That's right! It will be OpenRelatedAssetsAction containing embedded centre! And if you recall what was the context for OpenRelatedAssetsAction entity itself, it indeed has its own master entity of type Asset. So we have defined our first nested context!

Okay, but how can we get Asset from that nested context inside query enhancer? Let's have a look how we can do this:

import static ua.com.fielden.platform.entity.IContextDecomposer.decompose;
...

    private static class RelatedOpenAssetsCentreQueryEnhancer implements IQueryEnhancer<Asset> {
        @Override
        public ICompleted<Asset> enhanceQuery(final IWhere0<Asset> where, final Optional<CentreContext<Asset, ?>> context) {
            final Asset asset = decompose(context).ofMasterEntity().masterEntity(Asset.class);
            final String property = decompose(context).ofMasterEntity().chosenProperty();
            final IWhere0<Asset> queryPart = where
                .prop("active").eq().val(true).and()
                .prop("status").in().model(IAssetStatus.notDisposedOrCompletedStatuses).and();
            if ("parent".equals(property)) {
                return queryPart.prop("parent").eq().val(asset.getParent());
            } else if ("peer".equals(property)) {
                return queryPart.prop("peer").eq().val(asset.getPeer());
            } else if ("capitalProject".equals(property)) {
                return queryPart.prop("capitalProject").eq().val(asset.getCapitalProject());
            } else {
                throw new AssetModuleException(format("Action to open related open assets is not available for [%s] property."));
            }
        }
    }

That was pretty straightforward. We started decomposing context with

decompose(context). ... call and then have taken master entity of type Asset

... .masterEntity(Asset.class) from centre's master entity

... .ofMasterEntity(). ....

Please also note how we get chosen property:

final String property = decompose(context).ofMasterEntity().chosenProperty();

We did not add chosen property into context configuration for OpenRelatedAssetsAction -- there is no such API. Chosen property data is so tiny that it is included in every context, but only where applicable: in property actions on masters and property actions on centres.

Clone this wiki locally