diff --git a/bundles/af-core/pom.xml b/bundles/af-core/pom.xml index 005a72f7b5..724b01a800 100644 --- a/bundles/af-core/pom.xml +++ b/bundles/af-core/pom.xml @@ -472,6 +472,13 @@ apis + + com.adobe.aem + sites-dataplane + 1.0.113-SNAPSHOT + provided + + diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/dataplane/FormsComponentDefinitionEnricher.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/dataplane/FormsComponentDefinitionEnricher.java new file mode 100644 index 0000000000..7e1c7867ab --- /dev/null +++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/dataplane/FormsComponentDefinitionEnricher.java @@ -0,0 +1,263 @@ +package com.adobe.cq.forms.core.components.internal.dataplane; + +import com.adobe.aem.wcm.dataplane.openapimodels.pages.PageContentDefinitionComponentDefinitionsInner; +import com.adobe.aem.wcm.dataplane.openapimodels.pages.PageContentDefinitionComponentDefinitionsInnerFieldsInner; +import com.adobe.aem.wcm.dataplane.openapimodels.pages.PageContentDefinitionComponentDefinitionsInnerFieldsInnerOptionsInner; +import com.adobe.aem.wcm.dataplane.schema.spi.ComponentDefinitionEnricher; +import com.adobe.aem.wcm.dataplane.schema.spi.ComponentDefinitionEnrichmentContext; +import com.adobe.aemds.guide.model.FormMetaData; +import com.day.cq.wcm.foundation.forms.FormsManager; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ValueMap; +import org.osgi.service.component.annotations.Component; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Component(service = ComponentDefinitionEnricher.class) +public class FormsComponentDefinitionEnricher implements ComponentDefinitionEnricher { + + private static final String GUIDE_CONTAINER_V1 = "core/fd/components/form/container/v1/container"; + private static final String GUIDE_CONTAINER_V2 = "core/fd/components/form/container/v2/container"; + private static final String GUIDE_CONTAINER_EDS = "fd/franklin/components/form/v1/form"; + private static final String ACTION_TYPE_FIELD = "./actionType"; + private static final String PREFILL_SERVICE_FIELD = "./prefillService"; + private static final String DATASOURCE_NODE = "datasource"; + private static final String GUIDE_DATA_MODEL = "guideDataModel"; + private static final String NONE_OPTION_LABEL = "None"; + + @Override + public void enrich(ComponentDefinitionEnrichmentContext context) { + FormMetaData formMetaData = context.getResourceResolver().adaptTo(FormMetaData.class); + if (formMetaData == null) { + return; + } + + Map componentDefinitions = + context.getComponentDefinitions(); + componentDefinitions.entrySet().stream() + .filter(entry -> isGuideContainerComponent(entry.getValue())) + .map(Map.Entry::getKey) + .collect(Collectors.toList()) + .forEach(componentType -> enrichContainer(componentType, componentDefinitions, context, formMetaData)); + } + + private void enrichContainer( + String componentType, + Map componentDefinitions, + ComponentDefinitionEnrichmentContext context, + FormMetaData formMetaData) { + PageContentDefinitionComponentDefinitionsInner containerDefinition = componentDefinitions.get(componentType); + if (containerDefinition == null || containerDefinition.getFields() == null) { + return; + } + + setDynamicOptions( + containerDefinition, + ACTION_TYPE_FIELD, + collectSubmitActionOptions(formMetaData, componentType, context, componentDefinitions) + ); + setDynamicOptions( + containerDefinition, + PREFILL_SERVICE_FIELD, + collectPrefillServiceOptions(formMetaData, context, componentDefinitions) + ); + } + + private List collectSubmitActionOptions( + FormMetaData formMetaData, + String componentType, + ComponentDefinitionEnrichmentContext context, + Map componentDefinitions) { + String guideDataModel = resolveDatasourceProperty(componentType, ACTION_TYPE_FIELD, GUIDE_DATA_MODEL, + context.getResourceResolver()); + Set uniqueResourceTypes = new HashSet<>(); + List options = new ArrayList<>(); + Iterator iterator = formMetaData.getSubmitActions(); + + while (iterator != null && iterator.hasNext()) { + FormsManager.ComponentDescription description = iterator.next(); + if (description == null || !uniqueResourceTypes.add(description.getResourceType())) { + continue; + } + + Resource actionResource = findComponentResource(description.getResourceType(), context.getResourceResolver()); + if (!matchesGuideDataModel(actionResource, guideDataModel)) { + continue; + } + + options.add(option(resolveOptionLabel(description), description.getResourceType())); + addRuntimeDefinition(description.getResourceType(), componentDefinitions, context); + } + return options; + } + + private List collectPrefillServiceOptions( + FormMetaData formMetaData, + ComponentDefinitionEnrichmentContext context, + Map componentDefinitions) { + List options = new ArrayList<>(); + options.add(option(NONE_OPTION_LABEL, "")); + + Iterator iterator = formMetaData.getPrefillActions(); + while (iterator != null && iterator.hasNext()) { + FormsManager.ComponentDescription description = iterator.next(); + if (description == null) { + continue; + } + + options.add(option(resolveOptionLabel(description), description.getResourceType())); + addRuntimeDefinition(description.getResourceType(), componentDefinitions, context); + } + return options; + } + + private void addRuntimeDefinition( + String componentType, + Map componentDefinitions, + ComponentDefinitionEnrichmentContext context) { + if (componentType == null || componentType.isEmpty() || componentDefinitions.containsKey(componentType)) { + return; + } + + PageContentDefinitionComponentDefinitionsInner schema = context.resolveComponentDialog(componentType); + if (schema != null) { + componentDefinitions.put(componentType, schema); + } + } + + private void setDynamicOptions( + PageContentDefinitionComponentDefinitionsInner schema, + String fieldName, + List options) { + if (schema.getFields() == null || options == null || options.isEmpty()) { + return; + } + + schema.getFields().stream() + .filter(field -> fieldName.equals(field.getName())) + .findFirst() + .ifPresent(field -> field.setOptions(options)); + } + + private boolean isGuideContainerComponent(PageContentDefinitionComponentDefinitionsInner definition) { + if (definition == null) { + return false; + } + return isGuideContainerType(definition.getComponentType()) || isGuideContainerType(definition.getComponentSuperType()); + } + + private boolean isGuideContainerType(String componentType) { + if (componentType == null || componentType.isEmpty()) { + return false; + } + String normalizedComponentType = normalizeComponentType(componentType); + // TODO: Support EDS form container enrichment for fd/franklin/components/form/v1/form + // once AEM_EDGE content definition exposes compatible runtime-backed form properties. + return GUIDE_CONTAINER_V1.equals(normalizedComponentType) || GUIDE_CONTAINER_V2.equals(normalizedComponentType); + } + + private boolean matchesGuideDataModel(Resource actionResource, String guideDataModel) { + if (guideDataModel == null || guideDataModel.isEmpty()) { + return true; + } + if (actionResource == null) { + return false; + } + String supportedModels = actionResource.getValueMap().get(GUIDE_DATA_MODEL, ""); + return supportedModels.toLowerCase().contains(guideDataModel.toLowerCase()); + } + + private String resolveDatasourceProperty( + String componentType, + String fieldName, + String propertyName, + ResourceResolver resourceResolver) { + Resource componentResource = resolveComponentResource(componentType, resourceResolver); + if (componentResource == null) { + return ""; + } + + Resource dialogResource = componentResource.getChild("_cq_dialog"); + if (dialogResource == null) { + return ""; + } + + Resource fieldResource = findFieldResource(dialogResource, fieldName); + if (fieldResource == null) { + return ""; + } + + Resource datasource = fieldResource.getChild(DATASOURCE_NODE); + if (datasource == null) { + return ""; + } + + return datasource.getValueMap().get(propertyName, ""); + } + + private Resource resolveComponentResource(String componentType, ResourceResolver resourceResolver) { + if (componentType == null || componentType.isEmpty()) { + return null; + } + + String normalizedComponentType = normalizeComponentType(componentType); + Resource mergedComponentResource = resourceResolver.getResource("/mnt/override/" + normalizedComponentType); + if (mergedComponentResource != null) { + return mergedComponentResource; + } + + return findComponentResource(componentType, resourceResolver); + } + + private Resource findComponentResource(String componentType, ResourceResolver resourceResolver) { + if (componentType == null || componentType.isEmpty()) { + return null; + } + if (componentType.startsWith("/apps/") || componentType.startsWith("/libs/")) { + return resourceResolver.getResource(componentType); + } + + Resource appResource = resourceResolver.getResource("/apps/" + componentType); + if (appResource != null) { + return appResource; + } + return resourceResolver.getResource("/libs/" + componentType); + } + + private Resource findFieldResource(Resource resource, String fieldName) { + ValueMap properties = resource.getValueMap(); + if (fieldName.equals(properties.get("name", ""))) { + return resource; + } + + for (Resource child : resource.getChildren()) { + Resource match = findFieldResource(child, fieldName); + if (match != null) { + return match; + } + } + return null; + } + + private String normalizeComponentType(String componentType) { + return componentType.startsWith("/") ? componentType.substring(1) : componentType; + } + + private String resolveOptionLabel(FormsManager.ComponentDescription description) { + String title = description.getTitle(); + return title == null || title.isEmpty() ? description.getResourceType() : title; + } + + private PageContentDefinitionComponentDefinitionsInnerFieldsInnerOptionsInner option(String name, String value) { + return new PageContentDefinitionComponentDefinitionsInnerFieldsInnerOptionsInner() + .name(name) + .value(value); + } +} diff --git a/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/dataplane/FormsComponentDefinitionEnricherTest.java b/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/dataplane/FormsComponentDefinitionEnricherTest.java new file mode 100644 index 0000000000..904b09c088 --- /dev/null +++ b/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/dataplane/FormsComponentDefinitionEnricherTest.java @@ -0,0 +1,135 @@ +package com.adobe.cq.forms.core.components.internal.dataplane; + +import com.adobe.aem.wcm.dataplane.openapimodels.pages.PageContentDefinitionComponentDefinitionsInner; +import com.adobe.aem.wcm.dataplane.openapimodels.pages.PageContentDefinitionComponentDefinitionsInnerFieldsInner; +import com.adobe.aem.wcm.dataplane.schema.spi.ComponentDefinitionEnrichmentContext; +import com.adobe.aemds.guide.model.FormMetaData; +import com.day.cq.wcm.foundation.forms.FormsManager; +import io.wcm.testing.mock.aem.junit5.AemContext; +import io.wcm.testing.mock.aem.junit5.AemContextExtension; +import org.apache.sling.api.resource.ResourceResolver; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(AemContextExtension.class) +class FormsComponentDefinitionEnricherTest { + + private final AemContext context = new AemContext(); + + @Test + void testEnrich_AddsRuntimeOptionsAndDefinitions() { + ResourceResolver resourceResolver = context.resourceResolver(); + context.create().resource("/mnt/override/core/fd/components/form/container/v2/container/_cq_dialog/content/items/action", + "jcr:primaryType", "nt:unstructured", + "name", "./actionType"); + context.create().resource("/mnt/override/core/fd/components/form/container/v2/container/_cq_dialog/content/items/action/datasource", + "jcr:primaryType", "nt:unstructured", + "guideDataModel", "basic"); + context.create().resource("/mnt/override/core/fd/components/form/container/v2/container/_cq_dialog/content/items/prefill", + "jcr:primaryType", "nt:unstructured", + "name", "./prefillService"); + context.create().resource("/mnt/override/core/fd/components/form/container/v2/container/_cq_dialog/content/items/prefill/datasource", + "jcr:primaryType", "nt:unstructured"); + + context.create().resource("/libs/fd/af/components/guidesubmittype/restendpoint", + "jcr:primaryType", "cq:Component", + "guideDataModel", "basic"); + + FormMetaData formMetaData = Mockito.mock(FormMetaData.class); + Mockito.when(formMetaData.getSubmitActions()).thenReturn(iterator( + componentDescription("fd/af/components/guidesubmittype/restendpoint", "REST Endpoint") + )); + Mockito.when(formMetaData.getPrefillActions()).thenReturn(iterator( + componentDescription("com/adobe/forms/prefill/demo", "Demo Prefill") + )); + context.registerAdapter(ResourceResolver.class, FormMetaData.class, formMetaData); + + Map componentDefinitions = new LinkedHashMap<>(); + componentDefinitions.put("core/fd/components/form/container/v2/container", + schema("Container", "core/fd/components/form/container/v2/container", null, + field("./actionType"), field("./prefillService"))); + + ComponentDefinitionEnrichmentContext enrichmentContext = Mockito.mock(ComponentDefinitionEnrichmentContext.class); + Mockito.when(enrichmentContext.getResourceResolver()).thenReturn(resourceResolver); + Mockito.when(enrichmentContext.getComponentDefinitions()).thenReturn(componentDefinitions); + Mockito.when(enrichmentContext.resolveComponentDialog("fd/af/components/guidesubmittype/restendpoint")) + .thenReturn(schema("REST Endpoint", "fd/af/components/guidesubmittype/restendpoint", null, field("./restEndpointPostUrl"))); + Mockito.when(enrichmentContext.resolveComponentDialog("com/adobe/forms/prefill/demo")) + .thenReturn(schema("Demo Prefill", "com/adobe/forms/prefill/demo", null, field("./serviceUrl"))); + + new FormsComponentDefinitionEnricher().enrich(enrichmentContext); + + PageContentDefinitionComponentDefinitionsInner containerDefinition = + componentDefinitions.get("core/fd/components/form/container/v2/container"); + assertNotNull(containerDefinition); + + PageContentDefinitionComponentDefinitionsInnerFieldsInner actionTypeField = fieldByName(containerDefinition, "./actionType"); + assertNotNull(actionTypeField); + assertEquals(1, actionTypeField.getOptions().size()); + assertEquals("fd/af/components/guidesubmittype/restendpoint", actionTypeField.getOptions().get(0).getValue()); + + PageContentDefinitionComponentDefinitionsInnerFieldsInner prefillField = fieldByName(containerDefinition, "./prefillService"); + assertNotNull(prefillField); + assertEquals(2, prefillField.getOptions().size()); + assertEquals("", prefillField.getOptions().get(0).getValue()); + assertEquals("com/adobe/forms/prefill/demo", prefillField.getOptions().get(1).getValue()); + + assertTrue(componentDefinitions.containsKey("fd/af/components/guidesubmittype/restendpoint")); + assertTrue(componentDefinitions.containsKey("com/adobe/forms/prefill/demo")); + } + + private PageContentDefinitionComponentDefinitionsInner schema( + String title, + String componentType, + String componentSuperType, + PageContentDefinitionComponentDefinitionsInnerFieldsInner... fields) { + PageContentDefinitionComponentDefinitionsInner schema = new PageContentDefinitionComponentDefinitionsInner(); + schema.setTitle(title); + schema.setComponentType(componentType); + schema.setComponentSuperType(componentSuperType); + schema.setFields(new ArrayList<>(List.of(fields))); + return schema; + } + + private PageContentDefinitionComponentDefinitionsInnerFieldsInner field(String name) { + PageContentDefinitionComponentDefinitionsInnerFieldsInner field = new PageContentDefinitionComponentDefinitionsInnerFieldsInner(); + field.setName(name); + return field; + } + + private FormsManager.ComponentDescription componentDescription(String resourceType, String title) { + FormsManager.ComponentDescription description = Mockito.mock(FormsManager.ComponentDescription.class); + Mockito.when(description.getResourceType()).thenReturn(resourceType); + Mockito.when(description.getTitle()).thenReturn(title); + return description; + } + + @SafeVarargs + private final Iterator iterator(T... values) { + List list = new ArrayList<>(); + for (T value : values) { + list.add(value); + } + return list.iterator(); + } + + private PageContentDefinitionComponentDefinitionsInnerFieldsInner fieldByName( + PageContentDefinitionComponentDefinitionsInner definition, + String name) { + return definition.getFields().stream() + .filter(field -> name.equals(field.getName())) + .findFirst() + .orElse(null); + } +}