Backoffice widget to display itemtypes with indirect relation

In some cases, it is more practical to use an indirect reference between itemtypes by utilizing a string attribute, which stores code/uid/etc. This approach is especially useful in situations where regular relation between itemtypes are not possible or efficient.

Problem Description

There are several situations, where using indirect references is beneficial:

  1. Creation of related data before the main object: There are cases with master data system integrations, where related data must be created before the main object. For example, product-related information, such as price or classifications, may need to exist before the product itself. Similarly, addresses may need to be created before a B2BUnit.

  2. Handling removable or recreatable itemtypes: Some itemtypes may be removed or recreated, and maintaining the relationship between them without losing the reference is crucial. Examples include relationships between CartEntry and Product or PatchExecution and CronJob.

  3. Cross-catalog relations: Linking itemtypes across catalogs (e.g., between Product from product catalog and CMSItem from content catalog) can slow down or broke synchronization and lead to data issues. For example, products from the stage catalog might be mistakenly linked to CMS items from the online catalog, causing incorrect information to display on the storefront.

  4. Relation between catalog-aware and catalog-unaware itemtypes: Creating relation between catalog-unaware itemtypes with catalog-aware ones can result in mistakes, such as assigning items from the stage catalog. Such data issues, can’t be fixed with catalog synchronization, as it is not executed for catalog-unaware itemtypes.

In these scenarios, using a string-based code reference can be beneficial, as it prevents data mistakes and do not impact speed of catalog synchronization. However, this method has a disadvantage: backoffice users must manually input the code value and separately search for the related itemtypes. This process is less efficient than using a direct relation and OOTB Backoffice widgets for item selection and item viewing.

Technical Solution

To improve the user experience, a custom Backoffice widget can be created. This widget will extend the OOTB Default Reference Editor and allow users to select related items via a standard search interface, rather than manually entering the value of unique identifier, such as code.

The custom widget will include additional settings, such as the related itemtype and the attribute name of the related itemtype, enabling its reuse for various itemtypes (e.g., code attribute for Product code uid attribute for B2BUnit).

Implementation Details

Create backoffice extension

A custom Backoffice extension must first be created, if it does not already exist in the project. It can be created by following SAP’s guide. In current article, an extension called blogbackoffice will be used.

Create widget definition

A new widget called configurablereferenceeditor will be created, with widget definition stored in the definition.xml file within the resources/widgets/editors/configurablereferenceeditor folder. More information about widget creation can be found on SAP help portal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<!--
 Copyright (c) 2020 SAP SE or an SAP affiliate company. All rights reserved
-->
<editor-definition id="com.custom.editor.configurablereferenceeditor"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:noNamespaceSchemaLocation="http://www.hybris.com/schema/cockpitng/editor-definition.xsd"
                   extends="">

    <name>Configurable Reference Editor</name>
    <description>Configurable Reference Editor</description>
    <author>custom</author>
    <version>0.1</version>

    <type>^Reference\((.*)\)$</type>
    <editorClassName>com.blog.backoffice.editors.ConfigurableReferenceEditor</editorClassName>

    <sockets>
        <input id="referenceEditorInput"/>
        <output id="referenceEditorOutput" type="java.util.Map"/>
        <output id="referenceSelected" type="com.hybris.cockpitng.data.TypeAwareSelectionContext"/>
        <output id="referenceSearchCtx" type="com.hybris.cockpitng.data.TypeAwareSelectionContext"/>
    </sockets>

    <settings>
        <setting key="pageSize" default-value="5" type="Integer"/>
        <setting key="referenceAdvancedSearchEnabled" default-value="true" type="Boolean"/>
    </settings>
</editor-definition>

This definition extends the Default Reference Editor with a custom renderer class (ConfigurableReferenceEditor).

Implement widget renderer

Create a ConfigurableReferenceEditor renderer class in blogbackoffice extension:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package com.blog.backoffice.editors;

import com.google.common.collect.Iterables;
import com.hybris.cockpitng.editor.defaultreferenceeditor.DefaultReferenceEditor;
import com.hybris.cockpitng.editors.EditorContext;
import com.hybris.cockpitng.editors.EditorListener;
import de.hybris.platform.core.model.ItemModel;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.servicelayer.search.FlexibleSearchService;
import de.hybris.platform.servicelayer.search.SearchResult;
import org.apache.commons.lang.StringUtils;
import org.zkoss.zk.ui.Component;

import javax.annotation.Resource;
import java.util.List;

public class ConfigurableReferenceEditor extends DefaultReferenceEditor {

    @Resource
    private ModelService modelService;

    @Resource
    private FlexibleSearchService flexibleSearchService;

    @Override
    public void render(Component parent, EditorContext context, EditorListener listener) {
        Object referenceTypeObject = context.getParameter("referenceType");
        Object referenceAttributeObject = context.getParameter("referenceAttribute");
        if (!(referenceTypeObject instanceof String) || !(referenceAttributeObject instanceof String)) {
            return;
        }
        String referenceType = (String) referenceTypeObject;
        String referenceAttribute = (String) referenceAttributeObject;

        Object initialValue = resolveInitialValue(context, referenceType, referenceAttribute);
        EditorContext overridenContext = new EditorContext<>(initialValue,
                context.getDefinition(),
                context.getParameters(),
                context.getLabels(),
                context.getReadableLocales(),
                context.getWritableLocales());
        overridenContext.setValueType("Reference(" + referenceType + ")");
        overridenContext.setEditable(context.isEditable());

        super.render(parent, overridenContext, new ConfigurableReferenceEditorListener(listener, overridenContext));
    }

    private Object resolveInitialValue(EditorContext context, String referenceType, String referenceAttribute) {
        Object initialValue = context.getInitialValue();
        if (initialValue instanceof String) {

            SearchResult<Object> searchResult = flexibleSearchService.search(
                    createInitialValueSearchQuery(referenceType,
                            referenceAttribute,
                            (String) initialValue)
            );
            List<Object> result = searchResult.getResult();

            initialValue = Iterables.getFirst(result, initialValue);
        }
        return initialValue;
    }

    private static String createInitialValueSearchQuery(String referenceType, String referenceParameter, String value) {
        return "SELECT {pk} from {%s} WHERE {%s} = '%s'".formatted(referenceType, referenceParameter, value);
    }

    private class ConfigurableReferenceEditorListener implements EditorListener {

        private final EditorListener originalListener;
        private final EditorContext editorContext;

        private ConfigurableReferenceEditorListener(EditorListener originalListener, EditorContext editorContext) {
            this.originalListener = originalListener;
            this.editorContext = editorContext;
        }

        @Override
        public void onValueChanged(Object newValueObject) {
            Object parentObject = editorContext.getParameter("parentObject");
            if (parentObject instanceof ItemModel) {
                Object attributeValue = resolveAttributeValue(newValueObject);
                String resolveAttributeName = resolveAttributeName(editorContext);
                modelService.setAttributeValue(parentObject, resolveAttributeName, attributeValue);
            }

            originalListener.onValueChanged(newValueObject);
        }

        private String resolveAttributeName(EditorContext editorContext) {
            Object editorProperty = editorContext.getParameter("editorProperty");
            Object editorModelPrefix = editorContext.getParameter("editorModelPrefix");

            return StringUtils.removeStart(String.valueOf(editorProperty), editorModelPrefix + ".");
        }

        @Override
        public void onEditorEvent(String s) {
            originalListener.onEditorEvent(s);
        }

        @Override
        public void sendSocketOutput(String s, Object o) {
            originalListener.sendSocketOutput(s, o);
        }

        private Object resolveAttributeValue(Object object) {
            Object referenceAttribute = editorContext.getParameterAs("referenceAttribute");
            if (object instanceof ItemModel && referenceAttribute instanceof String) {
                return modelService.getAttributeValue(object, (String) referenceAttribute);
            }
            return null;
        }
    }
}

The ConfigurableReferenceEditor class extends the DefaultReferenceEditor to create a customized EditorContext that overrides the original context with predefined values for referenceType and referenceAttribute from blogbackoffice-backoffice-config.xml. These parameters define the type of reference being edited and the attribute related to that reference. If these parameters are valid strings, the method proceeds to resolve the initial value through the resolveInitialValue method. Once the overridden context is created it is passed to OOTB implementation with a custom listener that extends the original behavior.

The resolveInitialValue method determines the initial value of the editor based on the provided context. It constructs a flexible search query using the createInitialValueSearchQuery method(based on the referenceType and referenceAttribute configuration values) and searches for results. If a result is found, the first result is returned as the initial value. If no result is found, the original String value is returned. This allows the editor to dynamically resolve values from the database if it exists or just show persisted String value, if no related object were resolved.

The onValueChanged method in the custom ConfigurableReferenceEditorListener listener handles updates to the editor’s value. If the parentObject is an ItemModel, the method retrieves the referenceAttribute value and updates the object using the modelService. The change is then passed to the original listener for standard event handling. This approach allows the editor to dynamically resolve and update reference data while maintaining the original behavior of the default editor.

Configure widget connections

The custom widget needs to be connected to other widgets in the system, just like the OOTB Default Reference Editor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

<widget-connection sourceWidgetId="STUB_com.custom.editor.configurablereferenceeditor"
                   outputId="referenceEditorOutput" targetWidgetId="configurableFlow" inputId="context"/>
<widget-connection sourceWidgetId="configurableFlow" outputId="wizardResult"
                   targetWidgetId="STUB_com.custom.editor.configurablereferenceeditor"
                   inputId="referenceEditorInput"/>
<widget-connection sourceWidgetId="STUB_com.custom.editor.configurablereferenceeditor" outputId="referenceSelected"
                   targetWidgetId="collectionEditorAreaGroup" inputId="inputDataInput"/>
<widget-connection sourceWidgetId="STUB_com.custom.editor.configurablereferenceeditor" outputId="referenceSearchCtx"
                   targetWidgetId="referenceadvancedsearchgroup" inputId="referenceSearchCtx"/>
<widget-connection sourceWidgetId="referenceadvancedsearchgroup" outputId="selectedReferencesOutput"
                   targetWidgetId="STUB_com.custom.editor.configurablereferenceeditor"
                   inputId="referenceEditorInput"/>

How to use in backoffice configuration

The new widget can now be used in blogbackoffice-backoffice-config.xml. For this showcase, will be used the OOTB PatchExecution itemtype with dataMigrationJobCode, which stores the CronJob code (introduced in the Patching Framework article).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

<context type="PatchExecution" component="editor-area" merge-by="type">
    <editorArea:editorArea xmlns:editorArea="http://www.hybris.com/cockpitng/component/editorArea">
        <editorArea:tab name="blogbackoffice.PatchExecution.tab.implementation">
            <editorArea:section name="hmc.properties">
                <editorArea:attribute qualifier="implementationClass" readonly="true"/>
                <editorArea:attribute qualifier="patchType" readonly="true"/>

                <!-- Below is used Configurable Reference Editor for dataMigrationJobCode attribute. -->
                <editorArea:attribute qualifier="dataMigrationJobCode"
                                      label="blogbackoffice.PatchExecution.job.reference.label"
                                      editor="com.custom.editor.configurablereferenceeditor">
                    <editorArea:editor-parameter>
                        <editorArea:name>referenceType</editorArea:name>
                        <editorArea:value>CronJob</editorArea:value>
                    </editorArea:editor-parameter>
                    <editorArea:editor-parameter>
                        <editorArea:name>referenceAttribute</editorArea:name>
                        <editorArea:value>code</editorArea:value>
                    </editorArea:editor-parameter>
                </editorArea:attribute>

            </editorArea:section>
        </editorArea:tab>
    </editorArea:editorArea>
</context>

Here, the com.custom.editor.configurablereferenceeditor editor is defined for dataMigrationJobCode, with referenceType set to CronJob and referenceAttribute set to code.

comments powered by Disqus