From fc813fc18e0e27ba08b379b265f896e55f2bec3d Mon Sep 17 00:00:00 2001 From: Rishi Mehta <69448117+rismehta@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:57:06 +0530 Subject: [PATCH 001/424] Custom Transporthandler #donotmerge (#1476) * Transport handler * Custom transport handler * Adding replication agent in source code * Adding service user mapping and oauth 2 client * Fixing minor issues * Refactoring the code to keep the code more readable * Fixing vault filter rule * Fixing the dependency * Fixing latest addon and old core component build --- .../main/content/META-INF/vault/filter.xml | 1 + ...rImpl.amended~headlessreplication.cfg.json | 5 + .../main/content/META-INF/vault/filter.xml | 1 + .../main/content/jcr_root/etc/.content.xml | 4 + .../jcr_root/etc/replication/.content.xml | 9 + .../replication/agents.author/.content.xml | 6 + .../corecomponentsit/.content.xml | 19 ++ it/core/pom.xml | 6 + .../it/service/HeadlessTransportHandler.java | 222 ++++++++++++++++++ .../components/it/service/OAuth2Client.java | 138 +++++++++++ 10 files changed, 411 insertions(+) create mode 100644 it/config/src/main/content/jcr_root/apps/system/config/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~headlessreplication.cfg.json create mode 100755 it/content/src/main/content/jcr_root/etc/.content.xml create mode 100755 it/content/src/main/content/jcr_root/etc/replication/.content.xml create mode 100755 it/content/src/main/content/jcr_root/etc/replication/agents.author/.content.xml create mode 100755 it/content/src/main/content/jcr_root/etc/replication/agents.author/corecomponentsit/.content.xml create mode 100644 it/core/src/main/java/com/adobe/cq/forms/core/components/it/service/HeadlessTransportHandler.java create mode 100644 it/core/src/main/java/com/adobe/cq/forms/core/components/it/service/OAuth2Client.java diff --git a/it/config/src/main/content/META-INF/vault/filter.xml b/it/config/src/main/content/META-INF/vault/filter.xml index c1342e11ad..db6833e6df 100644 --- a/it/config/src/main/content/META-INF/vault/filter.xml +++ b/it/config/src/main/content/META-INF/vault/filter.xml @@ -5,4 +5,5 @@ + diff --git a/it/config/src/main/content/jcr_root/apps/system/config/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~headlessreplication.cfg.json b/it/config/src/main/content/jcr_root/apps/system/config/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~headlessreplication.cfg.json new file mode 100644 index 0000000000..e3585f27f6 --- /dev/null +++ b/it/config/src/main/content/jcr_root/apps/system/config/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~headlessreplication.cfg.json @@ -0,0 +1,5 @@ +{ + "user.mapping": [ + "com.adobe.aem.core-forms-components-it-tests-core:core-components-it-replication-sub-service=[replication-service]" + ] +} \ No newline at end of file diff --git a/it/content/src/main/content/META-INF/vault/filter.xml b/it/content/src/main/content/META-INF/vault/filter.xml index 3c0ff06e21..c6dfb60772 100644 --- a/it/content/src/main/content/META-INF/vault/filter.xml +++ b/it/content/src/main/content/META-INF/vault/filter.xml @@ -12,4 +12,5 @@ + diff --git a/it/content/src/main/content/jcr_root/etc/.content.xml b/it/content/src/main/content/jcr_root/etc/.content.xml new file mode 100755 index 0000000000..1581aa3428 --- /dev/null +++ b/it/content/src/main/content/jcr_root/etc/.content.xml @@ -0,0 +1,4 @@ + + diff --git a/it/content/src/main/content/jcr_root/etc/replication/.content.xml b/it/content/src/main/content/jcr_root/etc/replication/.content.xml new file mode 100755 index 0000000000..54909b7586 --- /dev/null +++ b/it/content/src/main/content/jcr_root/etc/replication/.content.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/it/content/src/main/content/jcr_root/etc/replication/agents.author/.content.xml b/it/content/src/main/content/jcr_root/etc/replication/agents.author/.content.xml new file mode 100755 index 0000000000..5580192f8c --- /dev/null +++ b/it/content/src/main/content/jcr_root/etc/replication/agents.author/.content.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/it/content/src/main/content/jcr_root/etc/replication/agents.author/corecomponentsit/.content.xml b/it/content/src/main/content/jcr_root/etc/replication/agents.author/corecomponentsit/.content.xml new file mode 100755 index 0000000000..e55cdbc22e --- /dev/null +++ b/it/content/src/main/content/jcr_root/etc/replication/agents.author/corecomponentsit/.content.xml @@ -0,0 +1,19 @@ + + + + diff --git a/it/core/pom.xml b/it/core/pom.xml index 462a280e46..c88f559f74 100644 --- a/it/core/pom.xml +++ b/it/core/pom.xml @@ -85,6 +85,7 @@ <_metatypeannotations>* javax.annotation;version=0.0.0, + com.adobe.cq.forms.core.components.models.form;version="[1.0.0,10.0.0)", * @@ -156,6 +157,11 @@ Import-Package: javax.annotation;version=0.0.0,* com.adobe.aem aem-forms-sdk-api + + com.adobe.aem + core-forms-components-af-core + 3.0.70 + diff --git a/it/core/src/main/java/com/adobe/cq/forms/core/components/it/service/HeadlessTransportHandler.java b/it/core/src/main/java/com/adobe/cq/forms/core/components/it/service/HeadlessTransportHandler.java new file mode 100644 index 0000000000..1d743239c7 --- /dev/null +++ b/it/core/src/main/java/com/adobe/cq/forms/core/components/it/service/HeadlessTransportHandler.java @@ -0,0 +1,222 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2024 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.forms.core.components.it.service; + +import com.adobe.cq.forms.core.components.models.form.FormStructureParser; +import com.adobe.cq.forms.core.components.util.ComponentUtils; +import com.day.cq.replication.AgentConfig; +import com.day.cq.replication.ReplicationAction; +import com.day.cq.replication.ReplicationException; +import com.day.cq.replication.ReplicationResult; +import com.day.cq.replication.ReplicationTransaction; +import com.day.cq.replication.TransportContext; +import com.day.cq.replication.TransportHandler; +import com.day.cq.wcm.api.NameConstants; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.osgi.services.HttpClientBuilderFactory; +import org.apache.sling.api.resource.LoginException; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceResolverFactory; +import org.apache.sling.serviceusermapping.ServiceUserMapped; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * Agent needs to be configured as per this, https://medium.com/@toimrank/aem-transporthandler-e761accaec51 + * https://blog.developer.adobe.com/reimagining-replication-agents-on-aem-as-a-cloud-service-a4437b7eeb60 + */ + +@Component( + service = TransportHandler.class, + property = { + "service.ranking:Integer=1000" + } +) +public class HeadlessTransportHandler implements TransportHandler { + + // todo: embedding credentials in source code risks unauthorized access + private static final String CLIENT_ID = "your_client_id"; + private static final String CLIENT_SECRET = "your_client_secret"; + + private static final Logger LOG = LoggerFactory.getLogger(HeadlessTransportHandler.class); + private static final Map AUTH; + private final static String URI = "corecomponentsitheadless"; + /** + * The Sling ServiceUserMapper service allows for mapping Service IDs comprised of the Service + * Names defined by the providing bundles and optional Subservice Name to ResourceResolver and/or + * JCR Repository user IDs. This mapping is configurable such that system administrators are in + * full control of assigning users to services. cf. http://sling.apache.org/documentation/the-sling-engine/service-authentication.html#implementation + */ + private final static String USER_MAPPED_SUB_SERVICE_NAME = "core-components-it-replication-sub-service"; + + static { + AUTH = new HashMap<>(); + // name of subservice, this is part of ui.config + AUTH.put(ResourceResolverFactory.SUBSERVICE, USER_MAPPED_SUB_SERVICE_NAME); + } + + @Reference + private HttpClientBuilderFactory clientBuilderFactory; + @Reference + private ResourceResolverFactory resourceResolverFactory; + + private CloseableHttpClient httpClient; + + @Activate + protected void activate() { + PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); + connectionManager.setDefaultMaxPerRoute(100); + connectionManager.setMaxTotal(100); + + httpClient = clientBuilderFactory.newBuilder() + .setConnectionManager(connectionManager) + .setDefaultRequestConfig(RequestConfig.custom() + .setConnectTimeout(30000) + .setSocketTimeout(30000) + .setConnectionRequestTimeout(30000) + .build()) + .build(); + } + + @Deactivate + protected void deactivate() { + try { + httpClient.close(); + } catch (IOException ex) { + LOG.warn("[HeadlessTransportHandler] Failed to release http client: {}", ex.getMessage(), ex); + } + } + + @Override + public boolean canHandle(AgentConfig agentConfig) { + return StringUtils.equals(agentConfig.getTransportURI(), URI); + // for oauth 2, hence commenting this + //&& StringUtils.isNotEmpty(agentConfig.getTransportUser()) + // && StringUtils.isNotEmpty(agentConfig.getTransportPassword()); + + } + + @Override + public ReplicationResult deliver(TransportContext transportContext, ReplicationTransaction replicationTransaction) + throws ReplicationException { + ReplicationAction action = replicationTransaction.getAction(); + Function requestSupplier; + + switch (action.getType()) { + case ACTIVATE: + requestSupplier = HttpPost::new; + break; + case DEACTIVATE: + case DELETE: + requestSupplier = HttpDelete::new; + break; + default: + LOG.debug("[HeadlessTransportHandler] Unsupported replication action type: {}", action); + return new ReplicationResult(true, 405, "Method Not Allowed"); + } + + AgentConfig agentConfig = transportContext.getConfig(); + /* + String transportUri = agentConfig.getTransportURI(); + String transportAuth = agentConfig.getTransportUser() + ':' + agentConfig.getTransportPassword(); + byte[] encodedAuth = Base64.encodeBase64(transportAuth.getBytes(StandardCharsets.ISO_8859_1)); + Header authHeader = new BasicHeader(HttpHeaders.AUTHORIZATION, "Basic " + new String(encodedAuth)); + Function authenticatedRequestSupplier = uri -> { + HttpRequestBase request = requestSupplier.apply(uri); + request.addHeader(authHeader); + return request; + }; */ + + try (ResourceResolver resourceResolver = resourceResolverFactory.getServiceResourceResolver(AUTH)) { + for (String path : action.getPaths()) { + Resource resource = resourceResolver.getResource(path); + if (resource == null || (!resource.isResourceType(NameConstants.NT_PAGE))) { + LOG.info("[HeadlessTransportHandler] Resource not found or not a cq:Page {}. Skipping", path); + continue; + } + // get the model json from the resource + FormStructureParser parser = getFormStructureParserFromPage(resource); + if (parser != null) { + String formModelJson = parser.getFormDefinition(); + // todo: publish this form model json to the external system + LOG.info("[HeadlessTransportHandler] Form Model JSON: {}", formModelJson); + /** + OAuth2Client oauth2Client = new OAuth2Client( + "https://example.com/oauth2/token", + "your_client_id", + "your_client_secret", + "https://example.com/api/publish", + httpClient + ); + oauth2Client.publishOrDeleteFormModelJson(formModelJson, requestSupplier); + **/ + } else { + LOG.info("[HeadlessTransportHandler] No adaptive form container found for resource {}. Skipping", resource.getPath()); + } + } + return ReplicationResult.OK; + } catch (LoginException /*| IOException */ ex) { + throw new ReplicationException("Failed to get delivery url for: " + action, ex); + } + } + + private static FormStructureParser getFormStructureParserFromPage(Resource resource) { + if (resource == null) { + LOG.info("[HeadlessTransportHandler] Resource is null. Skipping"); + return null; + } + + if (ComponentUtils.isAFContainer(resource)) { + FormStructureParser parser = resource.adaptTo(FormStructureParser.class); + if (parser != null) { + return parser; + } else { + LOG.info("[HeadlessTransportHandler] Form structure parser not found for form container resource {}. Skipping", resource.getPath()); + return null; + } + } + + for (Resource child : resource.getChildren()) { + FormStructureParser parser = getFormStructureParserFromPage(child); + if (parser != null) { + return parser; + } + } + return null; + } + + private static class NotOk extends IOException { + NotOk(int status) { + super("status code = " + status); + } + } +} diff --git a/it/core/src/main/java/com/adobe/cq/forms/core/components/it/service/OAuth2Client.java b/it/core/src/main/java/com/adobe/cq/forms/core/components/it/service/OAuth2Client.java new file mode 100644 index 0000000000..9f999479ee --- /dev/null +++ b/it/core/src/main/java/com/adobe/cq/forms/core/components/it/service/OAuth2Client.java @@ -0,0 +1,138 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2024 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.forms.core.components.it.service; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.JsonReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Function; + + +public class OAuth2Client { + private static final Logger LOG = LoggerFactory.getLogger(OAuth2Client.class); + + private final String tokenEndpoint; + private final String clientId; + private final String clientSecret; + private final String apiEndpoint; + private final CloseableHttpClient httpClient; + + private String accessToken; + private long tokenExpirationTime; + private final ReentrantLock lock = new ReentrantLock(); + + public OAuth2Client(String tokenEndpoint, String clientId, String clientSecret, String apiEndpoint, CloseableHttpClient httpClient) { + this.tokenEndpoint = tokenEndpoint; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.apiEndpoint = apiEndpoint; + this.httpClient = httpClient; + } + + public void publishOrDeleteFormModelJson(String formModelJson, Function requestSupplier) throws IOException { + String token = getValidToken(); + HttpRequestBase request = requestSupplier.apply(apiEndpoint); + request.setHeader("Authorization", "Bearer " + token); + request.setHeader("Content-Type", "application/json"); + if (request instanceof HttpPost) { + ((HttpPost) request).setEntity(new StringEntity(formModelJson)); + } + + try (CloseableHttpResponse response = httpClient.execute(request)) { + if (response.getStatusLine().getStatusCode() == 401) { + // Token expired, refresh and retry + token = refreshOAuth2Token(); + request.setHeader("Authorization", "Bearer " + token); + try (CloseableHttpResponse retryResponse = httpClient.execute(request)) { + if (retryResponse.getStatusLine().getStatusCode() != 200) { + throw new NotOk(retryResponse.getStatusLine().getStatusCode()); + } + } + } else if (response.getStatusLine().getStatusCode() != 200) { + throw new NotOk(response.getStatusLine().getStatusCode()); + } + } + } + + private String getValidToken() throws IOException { + lock.lock(); + try { + if (accessToken == null || System.currentTimeMillis() >= tokenExpirationTime) { + accessToken = fetchOAuth2Token(); + } + return accessToken; + } finally { + lock.unlock(); + } + } + + private String fetchOAuth2Token() throws IOException { + HttpPost post = new HttpPost(tokenEndpoint); + post.setHeader("Content-Type", "application/x-www-form-urlencoded"); + post.setEntity(new StringEntity("grant_type=client_credentials&client_id=" + clientId + "&client_secret=" + clientSecret)); + + try (CloseableHttpResponse response = httpClient.execute(post)) { + if (response.getStatusLine().getStatusCode() == 200) { + String responseBody = EntityUtils.toString(response.getEntity()); + return parseToken(responseBody); + } else { + throw new NotOk(response.getStatusLine().getStatusCode()); + } + } + } + + private String refreshOAuth2Token() throws IOException { + HttpPost post = new HttpPost(tokenEndpoint); + post.setHeader("Content-Type", "application/x-www-form-urlencoded"); + post.setEntity(new StringEntity("grant_type=refresh_token&refresh_token=your_refresh_token&client_id=" + clientId + "&client_secret=" + clientSecret)); + + try (CloseableHttpResponse response = httpClient.execute(post)) { + if (response.getStatusLine().getStatusCode() == 200) { + String responseBody = EntityUtils.toString(response.getEntity()); + return parseToken(responseBody); + } else { + throw new NotOk(response.getStatusLine().getStatusCode()); + } + } + } + + private String parseToken(String responseBody) { + try (JsonReader jsonReader = Json.createReader(new StringReader(responseBody))) { + JsonObject jsonObject = jsonReader.readObject(); + long expiresIn = jsonObject.getJsonNumber("expires_in").longValue(); + tokenExpirationTime = System.currentTimeMillis() + (expiresIn * 1000) - 60000; // 1 minute buffer + return jsonObject.getString("access_token"); + } + } + + private static class NotOk extends IOException { + NotOk(int status) { + super("status code = " + status); + } + } +} From aeed3f4805cb2ac1da35a2401e74f8abf1d88482 Mon Sep 17 00:00:00 2001 From: Rishi Mehta <69448117+rismehta@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:45:24 +0530 Subject: [PATCH 002/424] @releng moving to latest rotary image (#1481) --- .circleci/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/docker-compose.yml b/.circleci/docker-compose.yml index bab7fe455a..2416954443 100644 --- a/.circleci/docker-compose.yml +++ b/.circleci/docker-compose.yml @@ -6,7 +6,7 @@ services: entrypoint: tail -f /dev/null # Keeps the container running circleci-aem-cloudready: - image: docker-adobe-cif-release.dr-uw2.adobeitc.com/circleci-aem-cloudready:c3484b0d7a-openjdk11 + image: docker-adobe-cif-release.dr-uw2.adobeitc.com/circleci-aem-cloudready:9aa621f43a-openjdk11 depends_on: - circleci-qp # Add any additional configurations or environment variables if needed From 78b9db7fb8f13af54d6977fb2c113603949fa474 Mon Sep 17 00:00:00 2001 From: Pankaj Girotra <126753223+girotraapankaj@users.noreply.github.com> Date: Tue, 19 Nov 2024 14:31:15 +0530 Subject: [PATCH 003/424] Forms-17238 Fix link component to support spaces in asset path (#1478) * Forms-17238 Fix link component to support spaces in asset path * Forms-17238 Fix link component to support spaces in asset path * Forms-17238 Fix link component to support spaces in asset path --- .../models/v1/formsportal/LinkImpl.java | 25 +++++++++++++------ .../v1/formsportal/link/LinkImplTest.java | 14 ++++++++++- .../src/test/resources/link/test-content.json | 17 ++++++++++++- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/bundles/core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/formsportal/LinkImpl.java b/bundles/core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/formsportal/LinkImpl.java index 777c0021c6..825784ccfc 100644 --- a/bundles/core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/formsportal/LinkImpl.java +++ b/bundles/core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/formsportal/LinkImpl.java @@ -102,8 +102,7 @@ public String getAssetPathWithQueryParams() { return "#"; } try { - URIBuilder uriBuilder = null; - uriBuilder = new URIBuilder(url); + URIBuilder uriBuilder = new URIBuilder(url); Map queryParams = getQueryParams(); if (queryParams != null && !uriBuilder.isPathEmpty()) { for (String key : queryParams.keySet()) { @@ -118,7 +117,7 @@ public String getAssetPathWithQueryParams() { } url = uriBuilder.build().toString(); } catch (URISyntaxException e) { - logger.error("[FORMS] Link Component Failed to parse assetPath {}", url, e); + logger.warn("The [Forms] link component failed to process the asset path {}. Parameters will not be added to the URL.", url, e); } return url; } @@ -199,16 +198,28 @@ public Boolean accepts(LinkImpl link) { @Override public String processLink(LinkImpl link, SlingHttpServletRequest request) { String givenPath = link.getAssetPath(); - String builtPath = givenPath + "/" + JcrConstants.JCR_CONTENT; + String encodedPath = encodePath(givenPath); + String builtPath = encodedPath + "/" + JcrConstants.JCR_CONTENT; ResourceResolver resourceResolver = request.getResourceResolver(); - if (resourceResolver.getResource(builtPath) != null) { + if (resourceResolver.getResource(givenPath + "/" + JcrConstants.JCR_CONTENT) != null) { Map params = link.getQueryParams(); if (AssetType.ADAPTIVE_FORM.equals(link.getAssetType()) && !params.containsKey(QP_AF_DEFAULT_MODE_KEY)) { builtPath += "?" + QP_AF_DEFAULT_MODE_KEY + "=" + QP_AF_DEFAULT_MODE_VALUE; } - givenPath = builtPath; + encodedPath = builtPath; + } + return encodedPath; + } + + private String encodePath(String path) { + try { + // Encode path to allow space in asset names using URIBuilder#setPath method + URIBuilder uriBuilder = new URIBuilder().setPath(path); + return uriBuilder.build().toString(); + } catch (URISyntaxException e) { + logger.warn("The [Forms] link component failed to process the asset path {} due to invalid path.", path, e); + return path; } - return givenPath; } @Override diff --git a/bundles/core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/formsportal/link/LinkImplTest.java b/bundles/core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/formsportal/link/LinkImplTest.java index 74623ff9c5..113b0500df 100644 --- a/bundles/core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/formsportal/link/LinkImplTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/formsportal/link/LinkImplTest.java @@ -40,6 +40,8 @@ public class LinkImplTest { private static final String LINK1_PATH = ROOT_PAGE + "/linkcomponent-v1"; private static final String LINK1_PATH_WITH_INVALID_LINK = ROOT_PAGE + "/linkcomponent-v1-invalidref"; + private static final String LINK2_PATH_WITH_SPACE_IN_ASSET_NAME = ROOT_PAGE + "/linkcomponent-v2-with-space-in-asset-name"; + @BeforeEach public void setUp() { context.load().json(TEST_BASE + FormsCoreComponentTestContext.TEST_CONTENT_JSON, CONTENT_ROOT); @@ -82,12 +84,22 @@ public void testLinkComponent() { public void testLinkComponentWithInvalidPath() { Link link = getLinkUnderTest(LINK1_PATH_WITH_INVALID_LINK); Assertions.assertEquals("https://www.adobe.com/", link.getAssetPath()); - Assertions.assertEquals("https://www.adobe.com/?hello", link.getAssetPathWithQueryParams()); + Assertions.assertEquals("/https://www.adobe.com/?hello", link.getAssetPathWithQueryParams()); Assertions.assertEquals("Link Component", link.getTitle()); Assertions.assertEquals("Some Hover Tooltip Text", link.getTooltip()); Assertions.assertEquals(Link.AssetType.ADAPTIVE_FORM, link.getAssetType()); } + @Test + public void testLinkComponentWithSpaceInAssetPath() { + Link link = getLinkUnderTest(LINK2_PATH_WITH_SPACE_IN_ASSET_NAME); + Assertions.assertEquals("/content/dam/formsanddocuments/sample form", link.getAssetPath()); + Assertions.assertEquals("/content/dam/formsanddocuments/sample%20form?hello=world", link.getAssetPathWithQueryParams()); + Assertions.assertEquals("Link Component", link.getTitle()); + Assertions.assertEquals("Some Hover Tooltip Text", link.getTooltip()); + Assertions.assertEquals(Link.AssetType.PDF, link.getAssetType()); + } + @Test public void testMainInterface() { Link linkMock = Mockito.mock(Link.class); diff --git a/bundles/core/src/test/resources/link/test-content.json b/bundles/core/src/test/resources/link/test-content.json index 06ae876ddf..830053e7e2 100644 --- a/bundles/core/src/test/resources/link/test-content.json +++ b/bundles/core/src/test/resources/link/test-content.json @@ -111,6 +111,21 @@ "title" : "Link Component", "sling:resourceType" : "core/fd/components/formsportal/link/v2/link", "assetType" : "Others" + }, + "linkcomponent-v2-with-space-in-asset-name":{ + "tooltip" : "Some Hover Tooltip Text", + "pdfPath" : "/content/dam/formsanddocuments/sample form", + "title" : "Link Component", + "sling:resourceType" : "core/fd/components/formsportal/link/v2/link", + "assetType" : "PDF", + "queryParams":{ + "jcr:primaryType" : "nt:unstructured", + "item0":{ + "jcr:primaryType" : "nt:unstructured", + "key" : "hello", + "value" : "world" + } + } } }, "dam" : { @@ -128,4 +143,4 @@ } } } -} \ No newline at end of file +} From 6a34c699355d1238c64a5094a9aaab2fa5d6f3bb Mon Sep 17 00:00:00 2001 From: Amit Kumar <33446537+amit-mnnit@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:21:35 +0530 Subject: [PATCH 004/424] [FORMS-8058] implement review component (#1468) * [FORMS-8058] implement review component add css for now * add test cases * fix review points * add test case * fix review points #2 * fix review points #3 --------- Co-authored-by: Amit Kumar --- .../internal/form/FormConstants.java | 6 + .../internal/models/v1/form/ReviewImpl.java | 76 ++++ .../servlets/ReviewDataSourceServlet.java | 135 ++++++ .../core/components/models/form/Review.java | 44 ++ .../components/models/form/package-info.java | 2 +- .../models/v1/form/ReviewImplTest.java | 87 ++++ .../servlets/ReviewDataSourceServletTest.java | 132 ++++++ .../form/review/datasource/test-content.json | 82 ++++ .../resources/form/review/test-content.json | 68 +++ .../components/form/review/.content.xml | 7 + .../components/form/review/_cq_template.xml | 7 + .../samples/review/.content.xml | 5 + .../samples/review/basic/.content.xml | 402 ++++++++++++++++++ .../samples/review/repeatability/.content.xml | 390 +++++++++++++++++ .../.content.xml | 2 +- .../fd/components/form/review/.content.xml | 3 + .../fd/components/form/review/v1/.content.xml | 5 + .../form/review/v1/review/.content.xml | 7 + .../form/review/v1/review/README.md | 59 +++ .../review/v1/review/_cq_dialog/.content.xml | 124 ++++++ .../form/review/v1/review/_cq_template.xml | 5 + .../review/v1/review/clientlibs/.content.xml | 3 + .../v1/review/clientlibs/site/.content.xml | 5 + .../review/v1/review/clientlibs/site/css.txt | 18 + .../review/clientlibs/site/css/reviewview.css | 34 ++ .../review/v1/review/clientlibs/site/js.txt | 18 + .../review/clientlibs/site/js/reviewview.js | 333 +++++++++++++++ .../form/review/v1/review/review.html | 50 +++ .../form/review/v1/review/review.js | 40 ++ .../v1/review/template/fieldTemplate.html | 23 + .../v1/review/template/panelTemplate.html | 25 ++ .../v1/review/template/plainTextTemplate.html | 21 + ui.frontend/clientlib-dev.config.cjs | 2 +- ui.frontend/package-lock.json | 30 +- ui.frontend/package.json | 2 +- ui.frontend/src/constants.js | 11 +- .../libs/commons/formsConstants.js | 3 +- .../specs/review/review.authoring.spec.js | 97 +++++ .../review.repeatability.runtime.spec.js | 245 +++++++++++ .../specs/review/review.runtime.spec.js | 172 ++++++++ 40 files changed, 2759 insertions(+), 21 deletions(-) create mode 100644 bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/ReviewImpl.java create mode 100644 bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/servlets/ReviewDataSourceServlet.java create mode 100644 bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/Review.java create mode 100644 bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/form/ReviewImplTest.java create mode 100644 bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/servlets/ReviewDataSourceServletTest.java create mode 100644 bundles/af-core/src/test/resources/form/review/datasource/test-content.json create mode 100644 bundles/af-core/src/test/resources/form/review/test-content.json create mode 100644 examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/review/.content.xml create mode 100644 examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/review/_cq_template.xml create mode 100644 it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/review/.content.xml create mode 100644 it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/review/basic/.content.xml create mode 100644 it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/review/repeatability/.content.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/.content.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/.content.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/.content.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/README.md create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/_cq_dialog/.content.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/_cq_template.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/clientlibs/.content.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/clientlibs/site/.content.xml create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/clientlibs/site/css.txt create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/clientlibs/site/css/reviewview.css create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/clientlibs/site/js.txt create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/clientlibs/site/js/reviewview.js create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/review.html create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/review.js create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/template/fieldTemplate.html create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/template/panelTemplate.html create mode 100644 ui.af.apps/src/main/content/jcr_root/apps/core/fd/components/form/review/v1/review/template/plainTextTemplate.html create mode 100644 ui.tests/test-module/specs/review/review.authoring.spec.js create mode 100644 ui.tests/test-module/specs/review/review.repeatability.runtime.spec.js create mode 100644 ui.tests/test-module/specs/review/review.runtime.spec.js diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java index 9e9af9feab..09986f6e91 100644 --- a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java +++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/form/FormConstants.java @@ -133,4 +133,10 @@ private FormConstants() { public static final String REQ_ATTR_REFERENCED_PATH = "referencedPage"; public static final String PROP_FRAGMENT_PATH = "fragmentPath"; + + /** The resource type for review v1 */ + public static final String RT_FD_FORM_REVIEW_V1 = RT_FD_FORM_PREFIX + "review/v1/review"; + + /* The resource type for the pre-selected the linked panel */ + public final static String RT_FD_FORM_REVIEW_DATASOURCE_V1 = RT_FD_FORM_PREFIX + "review/v1/datasource"; } diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/ReviewImpl.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/ReviewImpl.java new file mode 100644 index 0000000000..362447b4f2 --- /dev/null +++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/ReviewImpl.java @@ -0,0 +1,76 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2023 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.forms.core.components.internal.models.v1.form; + +import java.util.Arrays; +import java.util.Map; + +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.models.annotations.Exporter; +import org.apache.sling.models.annotations.Model; +import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy; +import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import com.adobe.cq.export.json.ComponentExporter; +import com.adobe.cq.export.json.ExporterConstants; +import com.adobe.cq.forms.core.components.internal.form.FormConstants; +import com.adobe.cq.forms.core.components.models.form.FieldType; +import com.adobe.cq.forms.core.components.models.form.Review; +import com.adobe.cq.forms.core.components.util.AbstractBaseImpl; + +@Model( + adaptables = { SlingHttpServletRequest.class, Resource.class }, + adapters = { Review.class, + ComponentExporter.class }, + resourceType = { FormConstants.RT_FD_FORM_REVIEW_V1 }) +@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION) +public class ReviewImpl extends AbstractBaseImpl implements Review { + + private static final String LINKED_PANEL_PROPERTY = "fd:linkedPanels"; + private static final String EDIT_ACTION_PROPERTY = "fd:editModeAction"; + + @ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL, name = "fd:linkedPanels") + @Nullable + private String[] linkedPanels; + + @ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL, name = "fd:editModeAction") + @Nullable + private String editModeAction; + + public String[] getLinkedPanels() { + return linkedPanels != null ? Arrays.copyOf(linkedPanels, linkedPanels.length) : new String[] {}; + } + + public String getEditModeAction() { + return editModeAction; + } + + @Override + public String getFieldType() { + return super.getFieldType(FieldType.PLAIN_TEXT); + } + + @Override + public @NotNull Map getProperties() { + Map customProperties = super.getProperties(); + customProperties.put(LINKED_PANEL_PROPERTY, getLinkedPanels()); + customProperties.put(EDIT_ACTION_PROPERTY, getEditModeAction()); + return customProperties; + } +} diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/servlets/ReviewDataSourceServlet.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/servlets/ReviewDataSourceServlet.java new file mode 100644 index 0000000000..bd73c36b6d --- /dev/null +++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/servlets/ReviewDataSourceServlet.java @@ -0,0 +1,135 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2024 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.forms.core.components.internal.servlets; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.servlet.Servlet; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.SyntheticResource; +import org.apache.sling.api.resource.ValueMap; +import org.apache.sling.api.wrappers.ValueMapDecorator; +import org.jetbrains.annotations.NotNull; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +import com.adobe.cq.export.json.ComponentExporter; +import com.adobe.cq.forms.core.components.internal.form.FormConstants; +import com.adobe.cq.forms.core.components.models.form.Base; +import com.adobe.cq.forms.core.components.models.form.Container; +import com.adobe.cq.forms.core.components.models.form.FormComponent; +import com.adobe.cq.forms.core.components.models.form.FormContainer; +import com.adobe.cq.forms.core.components.models.form.Label; +import com.adobe.cq.forms.core.components.util.ComponentUtils; +import com.adobe.granite.ui.components.ExpressionResolver; +import com.adobe.granite.ui.components.ds.DataSource; +import com.adobe.granite.ui.components.ds.SimpleDataSource; +import com.adobe.granite.ui.components.ds.ValueMapResource; + +@Component( + service = { Servlet.class }, + property = { + "sling.servlet.resourceTypes=" + FormConstants.RT_FD_FORM_REVIEW_DATASOURCE_V1, + "sling.servlet.methods=GET", + "sling.servlet.extensions=html" + }) +public class ReviewDataSourceServlet extends AbstractDataSourceServlet { + + /** + * Defines the form meta data type. Possible values: {@code submitAction}, + * {@code prefillServiceProvider} + * + * @todo: Add other metadata types here like fragment, actions etc + */ + @Reference + private transient ExpressionResolver expressionResolver; + + @NotNull + @Override + protected ExpressionResolver getExpressionResolver() { + return expressionResolver; + } + + @Override + protected void doGet(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response) { + + ResourceResolver resourceResolver = request.getResourceResolver(); + String componentInstancePath = request.getRequestPathInfo().getSuffix(); + List resources = new ArrayList<>(); + if (resourceResolver != null) { + Resource componentInstance = resourceResolver.getResource(componentInstancePath); + Resource formInstance = ComponentUtils.getFormContainer(componentInstance); + if (formInstance != null) { + FormContainer formContainer = formInstance.adaptTo(FormContainer.class); + List panels = ((List) getMultipleChildPanels(formContainer)) + .stream().filter(x -> "panel".equals(x.getFieldType())).collect(Collectors.toList()); + for (Base panel : panels) { + String name = panel != null ? panel.getName() : ""; + String title = ""; + if (panel != null) { + Label label = panel.getLabel(); + if (label != null) { + String value = label.getValue(); + if (value != null) { + title = value; + } + } + } + if (name != null && title != null) { + resources.add(getResourceForDropdownDisplay(resourceResolver, title, name)); + } + } + } + } + SimpleDataSource actionTypeDataSource = new SimpleDataSource(resources.iterator()); + request.setAttribute(DataSource.class.getName(), actionTypeDataSource); + } + + /** + * Retrieves a list of child panels that have at least two siblings. + * If a panel has fewer than two siblings, it will not be included in the returned list. + * + * @param formContainer the top-level form container + * @return a list of panels with at least two siblings + */ + private List getMultipleChildPanels(FormComponent formContainer) { + while (formContainer instanceof Container && ((Container) formContainer).getItems().size() == 1) { + formContainer = (FormComponent) ((Container) formContainer).getItems().get(0); + } + if (formContainer instanceof Container) { + return ((Container) formContainer).getItems(); + } + return new ArrayList<>(); + } + + private SyntheticResource getResourceForDropdownDisplay(ResourceResolver resourceResolver, String displayValue, + String dataValue) { + Map dropdownMap = new HashMap<>(); + dropdownMap.put("text", displayValue); + dropdownMap.put("value", dataValue); + ValueMap dropdownEntryVm = new ValueMapDecorator(dropdownMap); + return new ValueMapResource(resourceResolver, "", JcrConstants.NT_UNSTRUCTURED, dropdownEntryVm); + } +} diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/Review.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/Review.java new file mode 100644 index 0000000000..066a376a7d --- /dev/null +++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/Review.java @@ -0,0 +1,44 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2024 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.forms.core.components.models.form; + +import org.osgi.annotation.versioning.ConsumerType; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Defines the {@code Review} Sling Model used for the {@code /apps/core/fd/components/form/review} component. + * + * @since com.adobe.cq.forms.core.components.models 5.9.6 + */ +@ConsumerType +public interface Review extends Base { + + /** + * @return an array of linked panels to be reviewed on the review page. Each linked panel is the name of a panel that is linked to the + * review page. + * @since com.adobe.cq.forms.core.components.models.form 5.9.6 + */ + @JsonIgnore + String[] getLinkedPanels(); + + /** + * @return the edit mode action, which indicates whether edit button is visible on the review page at field, panel, both, or none + * @since com.adobe.cq.forms.core.components.models.form 5.9.6 + */ + @JsonIgnore + String getEditModeAction(); +} diff --git a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/package-info.java b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/package-info.java index 2bd01360b5..e143716fa4 100644 --- a/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/package-info.java +++ b/bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/package-info.java @@ -35,7 +35,7 @@ *

*/ -@Version("5.9.5") +@Version("5.9.6") package com.adobe.cq.forms.core.components.models.form; import org.osgi.annotation.versioning.Version; diff --git a/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/form/ReviewImplTest.java b/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/form/ReviewImplTest.java new file mode 100644 index 0000000000..4b8e902de1 --- /dev/null +++ b/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/form/ReviewImplTest.java @@ -0,0 +1,87 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2024 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ + +package com.adobe.cq.forms.core.components.internal.models.v1.form; + +import java.io.IOException; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import com.adobe.cq.forms.core.Utils; +import com.adobe.cq.forms.core.components.models.form.Review; +import com.adobe.cq.forms.core.context.FormsCoreComponentTestContext; +import io.wcm.testing.mock.aem.junit5.AemContext; +import io.wcm.testing.mock.aem.junit5.AemContextExtension; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(AemContextExtension.class) +public class ReviewImplTest { + + private final AemContext context = FormsCoreComponentTestContext.newAemContext(); + private static final String TEST_BASE = "/form/review"; + private static final String APPS_ROOT = "/apps"; + private static final String PATH_REVIEW = "/apps/formcontainer/wizard/panel2/review"; + + @BeforeEach + void setUp() throws Exception { + context.load().json(TEST_BASE + FormsCoreComponentTestContext.TEST_CONTENT_JSON, APPS_ROOT); + } + + @Test + void testGetEditAction() { + Review review = Utils.getComponentUnderTest(PATH_REVIEW, ReviewImpl.class, context); + assertEquals("field", review.getEditModeAction()); + } + + @Test + public void testGetLinkedPanelsWithNonNullArray() throws IOException { + Review review = Utils.getComponentUnderTest(PATH_REVIEW, ReviewImpl.class, context); + String[] linkedPanels = review.getLinkedPanels(); + String[] expectedLinkedPanels = context.resourceResolver().getResource(PATH_REVIEW).getValueMap().get("fd:linkedPanels", + String[].class); + assertNotNull(expectedLinkedPanels); + assertArrayEquals(expectedLinkedPanels, linkedPanels); + } + + @Test + public void testGetLinkedPanelsWithNullArray() { + Review review = new ReviewImpl(); + String[] linkedPanels = review.getLinkedPanels(); + assertNotNull(linkedPanels); + assertEquals(0, linkedPanels.length); + } + + @Test + void testGetProperties() { + Review review = Utils.getComponentUnderTest(PATH_REVIEW, ReviewImpl.class, context); + Map properties = review.getProperties(); + assertEquals("/apps/formcontainer/wizard/panel2/review", properties.get("fd:path")); + assertEquals("field", properties.get("fd:editModeAction")); + + } + + @Test + public void testGetFieldType() { + Review review = new ReviewImpl(); + String fieldType = review.getFieldType(); + assertEquals("plain-text", fieldType); + } + +} diff --git a/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/servlets/ReviewDataSourceServletTest.java b/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/servlets/ReviewDataSourceServletTest.java new file mode 100644 index 0000000000..2bcc46b70d --- /dev/null +++ b/bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/servlets/ReviewDataSourceServletTest.java @@ -0,0 +1,132 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2024 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.forms.core.components.internal.servlets; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.testing.mock.sling.servlet.MockRequestPathInfo; +import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest; +import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.adobe.cq.export.json.SlingModelFilter; +import com.adobe.cq.forms.core.context.FormsCoreComponentTestContext; +import com.adobe.granite.ui.components.ExpressionResolver; +import com.adobe.granite.ui.components.ds.DataSource; +import com.day.cq.wcm.api.NameConstants; +import com.day.cq.wcm.msm.api.MSMNameConstants; +import io.wcm.testing.mock.aem.junit5.AemContext; +import io.wcm.testing.mock.aem.junit5.AemContextExtension; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith({ AemContextExtension.class, MockitoExtension.class }) +public class ReviewDataSourceServletTest { + + public static final String RT_FD_FORM_REVIEW_DATASOURCE_V1 = "core/fd/components/form/review/v1/datasource"; + private static final String TEST_BASE = "/form/review/datasource"; + private static final String APPS_ROOT = "/apps"; + private String componentInstancePath = "/apps/formcontainer/wizard/panel2/review"; + private String componentInstancePath2 = "/apps/formcontainer2/panel2/review"; + + public final AemContext context = FormsCoreComponentTestContext.newAemContext(); + + @BeforeEach + public void setUp() { + context.load().json(TEST_BASE + FormsCoreComponentTestContext.TEST_CONTENT_JSON, APPS_ROOT); + context.registerService(SlingModelFilter.class, new SlingModelFilter() { + + private final Set IGNORED_NODE_NAMES = new HashSet() { + { + add(NameConstants.NN_RESPONSIVE_CONFIG); + add(MSMNameConstants.NT_LIVE_SYNC_CONFIG); + add("cq:annotations"); + } + }; + + @Override + public Map filterProperties(Map map) { + return map; + } + + @Override + public Iterable filterChildResources(Iterable childResources) { + return StreamSupport + .stream(childResources.spliterator(), false) + .filter(r -> !IGNORED_NODE_NAMES.contains(r.getName())) + .collect(Collectors.toList()); + } + }); + } + + @Test + public void testDoGet() { + + context.currentResource("/apps"); + ReviewDataSourceServlet reviewDataSourceServlet = new ReviewDataSourceServlet(); + MockSlingHttpServletRequest request = context.request(); + MockSlingHttpServletResponse response = context.response(); + MockRequestPathInfo mockRequestPathInfo = (MockRequestPathInfo) request.getRequestPathInfo(); + mockRequestPathInfo.setSuffix(componentInstancePath); + reviewDataSourceServlet.doGet(request, response); + DataSource dataSource = (com.adobe.granite.ui.components.ds.DataSource) request.getAttribute(DataSource.class.getName()); + assertNotNull(dataSource); + Resource resource = dataSource.iterator().next(); + assertEquals("Item 1", resource.getValueMap().get("text", String.class)); + assertEquals("item_1", resource.getValueMap().get("value", String.class)); + + } + + @Test + public void testFormContainerNull() { + + context.currentResource("/apps"); + ReviewDataSourceServlet reviewDataSourceServlet = new ReviewDataSourceServlet(); + MockSlingHttpServletRequest request = context.request(); + MockSlingHttpServletResponse response = context.response(); + MockRequestPathInfo mockRequestPathInfo = (MockRequestPathInfo) request.getRequestPathInfo(); + mockRequestPathInfo.setSuffix(componentInstancePath2); + reviewDataSourceServlet.doGet(request, response); + DataSource dataSource = (com.adobe.granite.ui.components.ds.DataSource) request.getAttribute(DataSource.class.getName()); + assertNotNull(dataSource); + assertFalse(dataSource.iterator().hasNext()); + + } + + @Mock + ExpressionResolver expressionResolver; + + @InjectMocks + private ReviewDataSourceServlet reviewDataSourceServlet; + + @Test + public void testGetExpressionResolver() { + context.currentResource("/apps"); + ExpressionResolver expressionResolver = reviewDataSourceServlet.getExpressionResolver(); + assertNotNull(expressionResolver); + } + +} diff --git a/bundles/af-core/src/test/resources/form/review/datasource/test-content.json b/bundles/af-core/src/test/resources/form/review/datasource/test-content.json new file mode 100644 index 0000000000..54c50522e2 --- /dev/null +++ b/bundles/af-core/src/test/resources/form/review/datasource/test-content.json @@ -0,0 +1,82 @@ +{ + "formcontainer": { + "jcr:primaryType": "nt:unstructured", + "sling:resourceType": "core/fd/components/form/container/v2/container", + "dorType": "none", + "fd:version": "2.1", + "fieldType": "form", + "thankYouOption": "page", + "themeRef": "/libs/fd/af/themes/canvas", + "title": "test-review", + "wizard": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:title": "Wizard", + "jcr:lastModifiedBy": "admin", + "jcr:created": "Wed Aug 16 2023 11:21:29 GMT+0530", + "name": "wizard1692165089931", + "jcr:lastModified": "Wed Aug 16 2023 11:21:29 GMT+0530", + "sling:resourceType": "core/fd/components/form/wizard/v1/wizard", + "fieldType": "panel", + "panel1": { + "fieldType": "panel", + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Item 1", + "layout": "responsiveGrid", + "name": "item_1", + "sling:resourceType": "core/fd/components/form/panelcontainer/v1/panelcontainer", + "textinput": { + "fieldType": "text-input", + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Text Input", + "name": "textinput1692165103863", + "sling:resourceType": "core/fd/components/form/textinput/v1/textinput" + } + }, + "panel2": { + "fieldType": "panel", + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Item 2", + "layout": "responsiveGrid", + "name": "item_2", + "sling:resourceType": "core/fd/components/form/panelcontainer/v1/panelcontainer", + "review": { + "fd:editModeAction": "field", + "fieldType": "plain-text", + "hideTitle": "false", + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Review", + "name": "textinput1692165103863", + "sling:resourceType": "core/fd/components/form/review/v1/review" + } + } + } + }, + "formcontainer2": { + "jcr:primaryType": "nt:unstructured", + "sling:resourceType": "core/fd/components/form/container/v2/container", + "dorType": "none", + "fd:version": "2.1", + "fieldType": "form", + "thankYouOption": "page", + "themeRef": "/libs/fd/af/themes/canvas", + "title": "test-review", + "panel2": { + "fieldType": "panel", + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Item 2", + "layout": "responsiveGrid", + "name": "item_2", + "sling:resourceType": "core/fd/components/form/panelcontainer/v1/panelcontainer", + "review": { + "fd:editModeAction": "field", + "fieldType": "plain-text", + "hideTitle": "false", + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Review", + "name": "textinput1692165103863", + "sling:resourceType": "core/fd/components/form/review/v1/review" + } + } + } +} \ No newline at end of file diff --git a/bundles/af-core/src/test/resources/form/review/test-content.json b/bundles/af-core/src/test/resources/form/review/test-content.json new file mode 100644 index 0000000000..3dc1d85acb --- /dev/null +++ b/bundles/af-core/src/test/resources/form/review/test-content.json @@ -0,0 +1,68 @@ +{ + "formcontainer": { + "jcr:primaryType": "nt:unstructured", + "sling:resourceType": "core/fd/components/form/container/v2/container", + "dorType": "none", + "fd:version": "2.1", + "fieldType": "form", + "thankYouOption": "page", + "themeRef": "/libs/fd/af/themes/canvas", + "title": "test-review", + "wizard": { + "jcr:primaryType": "nt:unstructured", + "jcr:createdBy": "admin", + "jcr:title": "Wizard", + "jcr:lastModifiedBy": "admin", + "jcr:created": "Wed Aug 16 2023 11:21:29 GMT+0530", + "name": "wizard1692165089931", + "jcr:lastModified": "Wed Aug 16 2023 11:21:29 GMT+0530", + "sling:resourceType": "core/fd/components/form/wizard/v1/wizard", + "fieldType": "panel", + "panel1": { + "fieldType": "panel", + "id": "panel1", + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Item 1", + "layout": "responsiveGrid", + "name": "item_1", + "sling:resourceType": "core/fd/components/form/panelcontainer/v1/panelcontainer", + "textinput": { + "fieldType": "text-input", + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Text Input", + "name": "textinput1692165103863", + "sling:resourceType": "core/fd/components/form/textinput/v1/textinput" + } + }, + "panel2": { + "fieldType": "panel", + "id": "panel2", + "jcr:primaryType": "nt:unstructured", + "jcr:title": "Item 2", + "layout": "responsiveGrid", + "name": "item_2", + "sling:resourceType": "core/fd/components/form/panelcontainer/v1/panelcontainer", + "review" : { + "id": "review-c47310b241", + "jcr:primaryType": "nt:unstructured", + "sling:resourceType" : "core/fd/components/form/review/v1/review", + "name" : "review1691474151104", + "jcr:title" : "Review", + "description" : "test-description", + "fieldType" : "text", + "properties": { + "fd:path": "/apps/formcontainer/wizard/panel2/review", + "fd:linkedPanels": [ + "item_1" + ], + "fd:editModeAction": "field" + }, + "fd:linkedPanels": [ + "item_1" + ], + "fd:editModeAction": "field" + } + } + } + } +} \ No newline at end of file diff --git a/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/review/.content.xml b/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/review/.content.xml new file mode 100644 index 0000000000..a8d3af2ba6 --- /dev/null +++ b/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/review/.content.xml @@ -0,0 +1,7 @@ + + diff --git a/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/review/_cq_template.xml b/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/review/_cq_template.xml new file mode 100644 index 0000000000..1eed759959 --- /dev/null +++ b/examples/ui.apps/src/main/content/jcr_root/apps/forms-components-examples/components/form/review/_cq_template.xml @@ -0,0 +1,7 @@ + + diff --git a/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/review/.content.xml b/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/review/.content.xml new file mode 100644 index 0000000000..bbf62ed22c --- /dev/null +++ b/it/content/src/main/content/jcr_root/content/forms/af/core-components-it/samples/review/.content.xml @@ -0,0 +1,5 @@ + +