Dynamic Data Filtering for Relation Fields via Metadata Dependencies
Overview
In response of : https://crossmedia.atlassian.net/browse/WXM-19474
This feature provides the ability to filter data within Relation fields in the Front Office based on the values of other fields. By establishing these relationships, the selection in a dependency field (the source) dynamically restricts the available options in its dependent fields (the targets).
The mechanism relies on an event-driven update of the “~edit” cursor configuration, ensuring that the next fetch call for a relation field is automatically filtered by the backend logic.
Configuration
Dependencies are declared in the JSON configuration files. To make a field react to changes in another, add the dependsOn array to its definition.
Configuration Examples
keywordsfiltering bybrand(for “asset” library)
Path: src/wedia-portal-apps-configs/starter-kit/edit/asset/fields/names/keywords.json
{
"default": {
"dependsOn": ["brand"]
}
}coveragefiltering bybrand(for an other specific library)
Path: src/wedia-portal-apps-configs/starter-kit/edit/vvasset/fields/names/coverage.json
{
"default": {
"dependsOn": ["brand"]
}
}organizationfiltering bybrand(global default = applied to all the libraries)
Path: src/wedia-portal-apps-configs/starter-kit/edit/default/fields/names/organization.json
{
"default": {
"dependsOn": ["brand"]
}
}Technical Mechanism
1. Front Office: The Cursor Update
The interface uses specialized cursors to manage data fetching. The dependency flow works as follows:
Change Event: A user selects a value in a dependency field (e.g.,
brand).Cursor Injection: The interface identifies all dependent fields (e.g.,
keywords). It injects acustomParameter(prefixed withx_dependency_) specifically into the “~edit” cursors associated with those fields.Automatic Fetch: Updating the cursor automatically triggers a background fetch.
Ready State: When the user opens the "select" input for the dependent field, the filtered data is already loaded and ready to go.
The customParameter Payload:
The parameter (e.g., x_dependency_brand) contains a JSON string including:
id: The technical ID of the selected dependency.context: The editing context (edit,massimport, etc.).damobjects: The IDs of the assets currently being modified.
2. Backend Logic (Java Reference)
To interpret these parameters, you must implement a custom service. While the class naming is flexible, it must extend APIBaseWhereBusinessServiceAdapter. The process follows three main steps:
Interception: The service detects parameters starting with
x_dependency_.Parsing: It extracts the metadata IDs, context, and related assets from the JSON payload.
Query Modification: It builds a
PreparedWhereclause to restrict the results (e.g., adding anANDcondition to ensure only values linked to the selected dependency are returned).
Detailed documentation about APIBaseWhereBusinessServiceAdapter => https://crossmedia.atlassian.net/wiki/x/CIBPow
Implementation Hint: While you can tailor the filtering logic to your needs, the extractParams method provided in the example below is highly recommended. It is specifically designed to correctly parse the complex JSON payload (IDs, context, and asset arrays) sent by the Front Office.
package com.wedia.packaged.portals.extensions;
import com.noheto.preparedwhere.PreparedWhereService;
import com.noheto.restapi.APIBaseWhereBusinessServiceAdapter;
import com.noheto.restapi.Context;
import com.wedia.packaged.portals.utils.IPluginConstants;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import wsnoheto.engine.CTObjectStructure;
import wsnoheto.engine.CTSurfer;
import wsnoheto.engine.PreparedWhere;
import wsnoheto.engine.PreparedWhereException;
import wsnoheto.log.LoggingService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;
import java.util.stream.Collectors;
/**
* Example service to handle dynamic filtering based on dependency parameters.
* Note: Extending APIBaseWhereBusinessServiceAdapter is mandatory.
*/
public class AssetsFilterDependingFieldValues extends APIBaseWhereBusinessServiceAdapter {
private static final String DEP_CUSTOM_PARAM_PREFIX = "x_dependency_";
private List<String> depCustomParamsList = new ArrayList<>();
private static final ObjectMapper mapper = new ObjectMapper();
private static final Logger logger = LoggingService.getLogger(IPluginConstants.LOGGER_NAME);
/**
* Checks if the request contains parameters starting with x_dependency_
*/
private boolean doExecute(Context context) {
depCustomParamsList = context.getRequest()
.getParameterMap()
.keySet()
.stream()
.filter(key -> key.startsWith(DEP_CUSTOM_PARAM_PREFIX))
.collect(Collectors.toList());
return !depCustomParamsList.isEmpty();
}
@Override
public PreparedWhere getPreparedWhere(Context context) {
CTSurfer surfer = context.getSurfer();
String objectName = context.getObjectName();
if (doExecute(context)) {
try {
CTObjectStructure struct = CTObjectStructure.load(objectName);
// Retrieve the standard list query
PreparedWhere baseList = PreparedWhereService.getInstance()
.getPreparedWhere("base_list", surfer, objectName);
PreparedWhere finalPw = baseList;
for (String depCustomParam : depCustomParamsList) {
// Extract field name (e.g., x_dependency_brand -> brand)
String[] parts = depCustomParam.split(DEP_CUSTOM_PARAM_PREFIX);
String depField = parts.length > 1 ? parts[1] : null;
if (StringUtils.isNotBlank(depField) && struct.isExistField(depField)) {
// USEFUL: Utilizing the extractParams helper to get the JSON data
Map<String, Object> customParamValues = extractParams(context.getRequest().getParameterMap(), depCustomParam);
if (customParamValues != null && customParamValues.get("id") != null) {
String depId = customParamValues.get("id").toString();
// Build the filter: WHERE [depField] = [depId]
PreparedWhere depPw = new PreparedWhere("?").addChildEquals(depField, depId);
try {
finalPw.and(depPw);
} catch (PreparedWhereException e) {
logger.error("Error adding dependency filter: " + e.getMessage());
}
}
}
}
return finalPw;
} catch (Throwable e) {
logger.error("Error in dependency process: " + e.getMessage(), e);
}
}
return super.getPreparedWhere(context);
}
/**
* Highly useful utility method to parse the JSON payload from the Front Office.
* Extracts 'id', 'context', and 'damobjects' for easy use in filtering logic.
*/
public static Map<String, Object> extractParams(Map<String, String[]> paramMap, String key) {
Map<String, Object> result = new HashMap<>();
String rawJson = paramMap.get(key)[0];
if (StringUtils.isBlank(rawJson)) return null;
try {
JsonNode root = mapper.readTree(rawJson);
// Extracts the selected ID (path: value -> first element -> id)
String id = root.at("/value/0/id").asText(null);
// Extracts the editing context
String ctx = root.at("/context").asText(null);
// Extracts the list of currently edited assets
List<String> damObjects = new ArrayList<>();
root.at("/damobjects").forEach(n -> damObjects.add(n.asText()));
result.put("id", id);
result.put("context", ctx);
result.put("damobjects", damObjects);
} catch (Exception e) {
logger.error("Failed to parse dependency JSON payload: " + e.getMessage());
}
return result;
}
}
Behavior in Mass Editing
The system handles multiple asset selections by:
Triggering Refreshes: If the user modifies which assets are selected in a list, the “~edit” cursors for dependent fields are automatically updated.
Contextual Parameters: The
damobjectsandcontextparameters allow the backend service to apply different filtering rules (e.g., filtering options based on the specific Libraries of the selected assets).
Important Considerations
Data Consistency (Validation)
This evolution manages the available options for selection. It does not automatically clear or invalidate previously saved values if the dependency field is changed later. Final data coherence should be verified by the user.
Field Types: Optimized for Relation fields (
child,childmulti).Implementation Requirement: You must provide a Java service extending
APIBaseWhereBusinessServiceAdapterto process these parameters.