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:
-
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.
-
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
.
-
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.
-
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.
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
).
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.
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
.