3 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepage/body.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/META-INF/vault/filter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{java,xml}]
12 | indent_size = 4
13 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/all/src/main/content/META-INF/vault/filter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepagenext/body.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepage/clientlibs/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepagenext/clientlibs/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/DEV_GUIDELINES.md:
--------------------------------------------------------------------------------
1 | # aem-spa-project-core
2 |
3 | Library that provides a mapping that associates a component class with a resource path
4 |
5 | ## Development
6 |
7 | Simply run `mvn clean install`
8 |
9 | ### Build
10 |
11 | ```sh
12 | $ mvn clean install
13 | ```
14 |
15 | or
16 |
17 | ```sh
18 | $ npm clean package
19 | ```
20 |
21 | ### Test
22 |
23 | ```sh
24 | $ mvn surefire:test
25 | ```
26 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/page/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/xf-page/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/.github/workflows/security.yml:
--------------------------------------------------------------------------------
1 | name: Vulnerability check
2 | on:
3 | push:
4 | branches:
5 | - master
6 | pull_request_target:
7 |
8 | jobs:
9 | security:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout source code
13 | uses: actions/checkout@master
14 | - name: Run Snyk to check for vulnerabilities
15 | uses: snyk/actions/maven-3-jdk-11@master
16 | env:
17 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
18 | with:
19 | command: monitor
20 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepage/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepagenext/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/internal/impl/ExperienceFragmentPageExporter.java:
--------------------------------------------------------------------------------
1 | package com.adobe.aem.spa.project.core.internal.impl;
2 |
3 |
4 | import com.adobe.cq.export.json.ContainerExporter;
5 | import com.adobe.cq.export.json.hierarchy.HierarchyNodeExporter;
6 |
7 | /**
8 | * Interface that allows experience fragment pages (that cannot inherit the core component page) to be exported as model JSON
9 | * so that the SPA editor can function properly.
10 | */
11 | public interface ExperienceFragmentPageExporter extends ContainerExporter, HierarchyNodeExporter {
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[feature] "
5 | labels: feature-request
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[bug]"
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Package version**
14 | Provide a package version where the bug occurs.
15 |
16 | **To Reproduce**
17 | Steps to reproduce the behavior:
18 | 1. Go to '...'
19 | 2. Click on '....'
20 | 3. Scroll down to '....'
21 | 4. See error
22 |
23 | **Expected behavior**
24 | A clear and concise description of what you expected to happen.
25 |
26 | **Screenshots**
27 | If applicable, add screenshots to help explain your problem.
28 |
29 | **Additional context**
30 | Add any other context about the problem here.
31 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 | on: pull_request
3 |
4 | jobs:
5 | test:
6 | name: Build & Test
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout source code
10 | uses: actions/checkout@v2
11 | with:
12 | fetch-depth: 0
13 | - name: Setup Java
14 | uses: actions/setup-java@v1
15 | with:
16 | java-version: 11
17 | - name: Install dependencies and Build the project
18 | run: mvn clean install
19 | - name: Upload code coverage report to workflow as an artifact
20 | uses: actions/upload-artifact@v2
21 | with:
22 | name: core/target/site
23 | path: coverage
24 | - name: Upload code coverage report to codecov.io and comment in pull request
25 | uses: codecov/codecov-action@v1
26 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/models/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | @Version("1.1.0")
14 | package com.adobe.aem.spa.project.core.models;
15 |
16 | import org.osgi.annotation.versioning.Version;
17 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/models/RemotePage.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 | package com.adobe.aem.spa.project.core.models;
13 |
14 | import org.jetbrains.annotations.Nullable;
15 | import org.osgi.annotation.versioning.ConsumerType;
16 |
17 | /**
18 | * Defines the {@code RemotePage} Sling Model used for the {@code /apps/spa/project/core/models/remotepage} component
19 | */
20 | @ConsumerType
21 | public interface RemotePage extends Page {
22 | /**
23 | * @return the remote SPA page's URL, if one was set, or {@code null}
24 | */
25 | @Nullable
26 | default String getRemoteSPAUrl() {
27 | throw new UnsupportedOperationException();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/internal/impl/ExperienceFragmentPageImpl.java:
--------------------------------------------------------------------------------
1 | package com.adobe.aem.spa.project.core.internal.impl;
2 |
3 | import com.adobe.cq.export.json.ContainerExporter;
4 | import com.adobe.cq.export.json.ExporterConstants;
5 | import com.fasterxml.jackson.databind.annotation.JsonSerialize;
6 | import org.apache.sling.api.SlingHttpServletRequest;
7 | import org.apache.sling.models.annotations.Exporter;
8 | import org.apache.sling.models.annotations.Model;
9 | import org.apache.sling.models.annotations.Optional;
10 | import org.apache.sling.models.annotations.injectorspecific.Self;
11 |
12 | /**
13 | * JsonSerialize annotation is added to prevent errors being thrown in model.json export, since these don't work for this type of page.
14 | */
15 | @Model(adaptables = SlingHttpServletRequest.class, adapters = {ExperienceFragmentPageExporter.class,
16 | ContainerExporter.class}, resourceType = {
17 | ExperienceFragmentPageImpl.XF_RESOURCE_TYPE
18 | })
19 | @Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
20 | @JsonSerialize(as = ExperienceFragmentPageExporter.class)
21 | public class ExperienceFragmentPageImpl extends PageImpl implements ExperienceFragmentPageExporter {
22 |
23 | static final String XF_RESOURCE_TYPE = "spa-project-core/components/xf-page";
24 |
25 | /**
26 | * Override the getDelegate with an optional flag, so that it still can be adapted. We cannot inherit the core resource type on a XF page.
27 | */
28 | @Self
29 | @Optional
30 | protected com.adobe.cq.wcm.core.components.models.Page delegate;
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/page/page.html:
--------------------------------------------------------------------------------
1 |
12 |
13 |
21 |
22 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/internal/impl/ComponentContextRequestWrapper.java:
--------------------------------------------------------------------------------
1 | /* ************************************************************************
2 | * ADOBE CONFIDENTIAL
3 | * ___________________
4 | *
5 | * Copyright 2022 Adobe
6 | * All Rights Reserved.
7 | *
8 | * NOTICE: All information contained herein is, and remains
9 | * the property of Adobe and its suppliers, if any. The intellectual
10 | * and technical concepts contained herein are proprietary to Adobe
11 | * and its suppliers and are protected by all applicable intellectual
12 | * property laws, including trade secret and copyright laws.
13 | * Dissemination of this information or reproduction of this material
14 | * is strictly forbidden unless prior written permission is obtained
15 | * from Adobe.
16 | **************************************************************************/
17 | package com.adobe.aem.spa.project.core.internal.impl;
18 |
19 | import org.apache.sling.api.SlingHttpServletRequest;
20 | import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
21 |
22 | import com.day.cq.wcm.api.components.ComponentContext;
23 |
24 | /**
25 | * Custom request wrapper which allows to store {@link ComponentContext#CONTEXT_ATTR_NAME} attribute.
26 | */
27 | public class ComponentContextRequestWrapper extends SlingHttpServletRequestWrapper {
28 | private final Object componentContext;
29 |
30 | public ComponentContextRequestWrapper(SlingHttpServletRequest wrappedRequest, HierarchyComponentContextWrapper componentContext) {
31 | super(wrappedRequest);
32 | this.componentContext = componentContext;
33 | }
34 |
35 | @Override
36 | public Object getAttribute(String name) {
37 | return ComponentContext.CONTEXT_ATTR_NAME.equals(name) ? this.componentContext : super.getAttribute(name);
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/internal/impl/utils/ContentPolicyUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | package com.adobe.aem.spa.project.core.internal.impl.utils;
14 |
15 | import org.apache.sling.api.resource.ValueMap;
16 |
17 | import com.day.cq.wcm.api.policies.ContentPolicy;
18 |
19 | class ContentPolicyUtils {
20 | private ContentPolicyUtils() {
21 | }
22 |
23 | /**
24 | * Utility function which checks whether the provided content policy contains a property and whether its value is {@code true}
25 | *
26 | * @param contentPolicy The content policy which should be searched for the specified property
27 | * @param propertyName Name of the JCR property to check
28 | * @return Boolean indicating whether the property is set to {@code true}. If the content policy or the property don't exist, the
29 | * function will return {@code false}
30 | */
31 | static boolean propertyIsTrue(ContentPolicy contentPolicy, String propertyName) {
32 | if (contentPolicy != null) {
33 | ValueMap properties = contentPolicy.getProperties();
34 | return (properties != null && properties.get(propertyName, false));
35 | }
36 | return false;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SPA Project Core
2 |
3 | [](https://github.com/adobe/aem-spa-project-core/blob/master/LICENSE)
4 | [](https://mvnrepository.com/artifact/com.adobe.aem/spa.project.core)
5 |
6 |
7 |
8 | **This package contains the components required for building a single-page application using AEM.**
9 |
10 | It contains a `Page` interface (extension of the Core Components' `Page` v1 and v2), which adds support for a hierarchical model of subpages with which a single-page application can be built.
11 |
12 | The `PageImpl` allows the retrieval of a hierarchical page model in JSON format. The content of the exported model can be configured using parameters. More information can be found in [`PageImpl.java`](./core/src/main/java/com/adobe/aem/spa/project/core/internal/impl/PageImpl.java).
13 |
14 | ## Installation
15 | The dependency can be found here:
16 | https://mvnrepository.com/artifact/com.adobe.aem/aem.project.core
17 | Simply put the following in your pom.xml:
18 | ```
19 |
20 | com.adobe.aem
21 | spa.project.core
22 |
23 | pom
24 |
25 | ```
26 |
27 | ## Documentation
28 |
29 | * [SPA Editor Overview](https://www.adobe.com/go/aem6_5_docs_spa_en)
30 | * [SPA Architecture](https://docs.adobe.com/content/help/en/experience-manager-65/developing/headless/spas/spa-architecture.html)
31 | * [Getting Started with the AEM SPA Editor and Angular](https://docs.adobe.com/content/help/en/experience-manager-learn/spa-angular-tutorial/overview.html)
32 | * [Getting Started with the AEM SPA Editor and React](https://docs.adobe.com/content/help/en/experience-manager-learn/spa-react-tutorial/overview.html)
33 |
34 | ## Contributing
35 |
36 | Contributions are welcome! Read the [Contributing Guide](CONTRIBUTING.md) for more information.
37 |
38 | ## Licensing
39 |
40 | This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information.
41 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/internal/HierarchyConstants.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | package com.adobe.aem.spa.project.core.internal;
14 |
15 | public class HierarchyConstants {
16 |
17 | private HierarchyConstants() {
18 | }
19 |
20 | /**
21 | * URL extension specific to the Sling Model exporter
22 | */
23 | public static final String JSON_EXPORT_SUFFIX = ".model.json";
24 |
25 | /**
26 | * Is the current model to be considered as a model root
27 | */
28 | public static final String PN_IS_ROOT = "isRoot";
29 |
30 | /**
31 | * Name of the request attribute which is used to flag the child pages. Optionally available as a request attribute
32 | */
33 | public static final String ATTR_IS_CHILD_PAGE = "com.adobe.aem.spa.project.core.models.Page.isChildPage";
34 |
35 | /**
36 | * Name of the request attribute that defines whether the page is an entry point of the request.
37 | */
38 | public static final String ATTR_HIERARCHY_ENTRY_POINT_PAGE = "com.adobe.aem.spa.project.core.models.Page.entryPointPage";
39 |
40 | /**
41 | * Request attribute key of the component context
42 | */
43 | public static final String ATTR_COMPONENT_CONTEXT = "com.day.cq.wcm.componentcontext";
44 |
45 | /**
46 | * Request attribute key of the current page
47 | */
48 | public static final String ATTR_CURRENT_PAGE = "currentPage";
49 |
50 | /**
51 | * List of Regexp patterns to filter the exported tree of pages
52 | */
53 | public static final String PN_STRUCTURE_PATTERNS = "structurePatterns";
54 | }
55 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepage/remotepage.html:
--------------------------------------------------------------------------------
1 |
12 |
13 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/.github/workflows/settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
18 |
19 |
20 |
21 | ossrh
22 |
23 | true
24 |
25 |
26 | ossrh
27 | https://oss.sonatype.org/service/local/staging/deploy/maven2/
28 | ossrh
29 | https://oss.sonatype.org/content/repositories/snapshots
30 | github
31 | gpg
32 | ${env.GPG_PASSPHRASE}
33 |
34 |
35 |
36 |
37 |
38 |
39 | ossrh
40 | ${env.SONATYPE_USERNAME}
41 | ${env.SONATYPE_PASSWORD}
42 |
43 |
44 | github
45 | ${env.X_GITHUB_USERNAME}
46 | ${env.X_GITHUB_PASSWORD}
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/core/src/test/java/com/adobe/aem/spa/project/core/internal/impl/RemotePageImplTest.java:
--------------------------------------------------------------------------------
1 | package com.adobe.aem.spa.project.core.internal.impl;
2 |
3 | import com.adobe.aem.spa.project.core.models.RemotePage;
4 | import com.day.cq.wcm.api.PageManager;
5 | import org.apache.commons.lang3.reflect.FieldUtils;
6 | import org.apache.sling.api.SlingHttpServletRequest;
7 | import org.apache.sling.api.resource.ResourceResolver;
8 | import org.junit.jupiter.api.BeforeEach;
9 | import org.junit.jupiter.api.Test;
10 | import org.junit.jupiter.api.extension.ExtendWith;
11 | import org.mockito.InjectMocks;
12 | import org.mockito.Mock;
13 | import org.mockito.junit.jupiter.MockitoExtension;
14 | import org.mockito.junit.jupiter.MockitoSettings;
15 | import org.mockito.quality.Strictness;
16 |
17 | import static org.junit.jupiter.api.Assertions.assertEquals;
18 | import static org.junit.jupiter.api.Assertions.assertTrue;
19 | import static org.mockito.Mockito.when;
20 |
21 | @ExtendWith(MockitoExtension.class)
22 | @MockitoSettings(strictness = Strictness.LENIENT)
23 | public class RemotePageImplTest {
24 |
25 | class NotImplementedClass implements RemotePage{}
26 |
27 | @InjectMocks
28 | private RemotePageImpl page;
29 |
30 | @Mock
31 | SlingHttpServletRequest request;
32 |
33 | @Mock
34 | ResourceResolver resourceResolver;
35 |
36 | @Mock
37 | PageManager pageManager;
38 |
39 | @BeforeEach
40 | void beforeEach() {
41 | when(request.getResourceResolver()).thenReturn(resourceResolver);
42 | when(resourceResolver.adaptTo(PageManager.class)).thenReturn(pageManager);
43 | }
44 |
45 | @Test
46 | void testDefaultRemoteSpaUrl() {
47 | assertEquals("", page.getRemoteSPAUrl());
48 | }
49 |
50 | @Test
51 | void testGetRemoteSpaUrl() throws IllegalAccessException {
52 | FieldUtils.writeField(page,"remoteSPAUrl", "/dummy/url", true);
53 | // descendedPageModels is null
54 | assertEquals("/dummy/url", page.getRemoteSPAUrl());
55 | }
56 |
57 | @Test
58 | void testDefaultMethod(){
59 | try{
60 | new NotImplementedClass().getRemoteSPAUrl();
61 | assertTrue(false);
62 | }catch(UnsupportedOperationException ex){
63 | assertTrue(true);
64 | }
65 |
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for choosing to contribute!
4 |
5 | The following are a set of guidelines to follow when contributing to this project.
6 |
7 | ## Code Of Conduct
8 |
9 | This project adheres to the Adobe [code of conduct](../CODE_OF_CONDUCT.md). By participating,
10 | you are expected to uphold this code. Please report unacceptable behavior to
11 | [Grp-opensourceoffice@adobe.com](mailto:Grp-opensourceoffice@adobe.com).
12 |
13 | ## Have A Question?
14 |
15 | Start by filing an issue. The existing committers on this project work to reach
16 | consensus around project direction and issue solutions within issue threads
17 | (when appropriate).
18 |
19 | ## Contributor License Agreement
20 |
21 | All third-party contributions to this project must be accompanied by a signed contributor
22 | license agreement. This gives Adobe permission to redistribute your contributions
23 | as part of the project. [Sign our CLA](https://opensource.adobe.com/cla.html). You
24 | only need to submit an Adobe CLA one time, so if you have submitted one previously,
25 | you are good to go!
26 |
27 | ## Code Reviews
28 |
29 | All submissions should come in the form of pull requests and need to be reviewed
30 | by project committers. Read [GitHub's pull request documentation](https://help.github.com/articles/about-pull-requests/)
31 | for more information on sending pull requests.
32 |
33 | Lastly, please follow the [pull request template](PULL_REQUEST_TEMPLATE.md) when
34 | submitting a pull request!
35 |
36 | ## From Contributor To Committer
37 |
38 | We love contributions from our community! If you'd like to go a step beyond contributor
39 | and become a committer with full write access and a say in the project, you must
40 | be invited to the project. The existing committers employ an internal nomination
41 | process that must reach lazy consensus (silence is approval) before invitations
42 | are issued. If you feel you are qualified and want to get more deeply involved,
43 | feel free to reach out to existing committers to have a conversation about that.
44 |
45 | ## Security Issues
46 |
47 | Security issues shouldn't be reported on this issue tracker. Instead, [file an issue to our security experts](https://helpx.adobe.com/security/alertus.html).
48 |
49 | ## Developer Guidelines
50 |
51 | * [Developer Guidelines](DEV_GUIDELINES.md)
52 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepagenext/remotepagenext.html:
--------------------------------------------------------------------------------
1 |
12 |
13 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/page/_cq_design_dialog/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/internal/impl/utils/StyleUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | package com.adobe.aem.spa.project.core.internal.impl.utils;
14 |
15 | import org.jetbrains.annotations.NotNull;
16 | import org.jetbrains.annotations.Nullable;
17 |
18 | import com.adobe.aem.spa.project.core.internal.HierarchyConstants;
19 | import com.day.cq.wcm.api.designer.Style;
20 |
21 | public class StyleUtils {
22 |
23 | private StyleUtils() {
24 | }
25 |
26 | /**
27 | * Returns the tree depth that can be configured in the policy. Defaults to 0
28 | *
29 | * @param style Style to search in
30 | * @param pnStructureDepth Name of the structure depth attribute
31 | * @return The defined traversal depth or 0 if not defined
32 | */
33 | public static int getPageTreeDepth(Style style, String pnStructureDepth) {
34 | // Depth of the tree of pages
35 | Integer pageTreeTraversalDepth = getStructureDepth(style, pnStructureDepth);
36 |
37 | if (pageTreeTraversalDepth == null) {
38 | return 0;
39 | }
40 |
41 | return pageTreeTraversalDepth;
42 | }
43 |
44 | /**
45 | * Returns the style's structure depth attribute value
46 | *
47 | * @param style Style to search in
48 | * @param pnStructureDepth Name of the structure depth attribute
49 | * @return Structure depth attribute value
50 | */
51 | @Nullable
52 | public static Integer getStructureDepth(Style style, String pnStructureDepth) {
53 | if (style != null) {
54 | return style.get(pnStructureDepth, Integer.class);
55 | }
56 |
57 | return null;
58 | }
59 |
60 | public static boolean isRootPage(@NotNull Style style) {
61 | return style.get(HierarchyConstants.PN_IS_ROOT, false);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/core/src/test/java/com/adobe/aem/spa/project/core/internal/impl/utils/ContentPolicyUtilsTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | package com.adobe.aem.spa.project.core.internal.impl.utils;
14 |
15 | import org.apache.sling.api.resource.ValueMap;
16 | import org.junit.jupiter.api.BeforeEach;
17 | import org.junit.jupiter.api.Test;
18 | import org.junit.jupiter.api.extension.ExtendWith;
19 | import org.mockito.Mock;
20 | import org.mockito.Spy;
21 | import org.mockito.junit.jupiter.MockitoExtension;
22 | import org.mockito.junit.jupiter.MockitoSettings;
23 | import org.mockito.quality.Strictness;
24 |
25 | import com.day.cq.wcm.api.policies.ContentPolicy;
26 |
27 | import static org.junit.jupiter.api.Assertions.assertFalse;
28 | import static org.junit.jupiter.api.Assertions.assertTrue;
29 | import static org.mockito.Mockito.when;
30 |
31 | @ExtendWith(MockitoExtension.class)
32 | @MockitoSettings(strictness = Strictness.LENIENT)
33 | class ContentPolicyUtilsTest {
34 | private static final String PROPERTY_NAME = "propertyName";
35 |
36 | @Spy
37 | private ContentPolicy contentPolicy;
38 |
39 | @Mock
40 | private ValueMap properties;
41 |
42 | @BeforeEach
43 | void beforeEach() {
44 | when(contentPolicy.getProperties()).thenReturn(properties);
45 | }
46 |
47 | @Test
48 | void testPropertyIsTruePropertyMissingOrFalse() {
49 | // Property is missing or set to `false`
50 | when(properties.get(PROPERTY_NAME, false)).thenReturn(false);
51 | assertFalse(ContentPolicyUtils.propertyIsTrue(contentPolicy, PROPERTY_NAME));
52 | }
53 |
54 | @Test
55 | void testPropertyIsTruePropertyTrue() {
56 | // Property is set to `true`
57 | when(properties.get(PROPERTY_NAME, false)).thenReturn(true);
58 | assertTrue(ContentPolicyUtils.propertyIsTrue(contentPolicy, PROPERTY_NAME));
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/.github/workflows/maven-release.yml:
--------------------------------------------------------------------------------
1 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 | # Copyright 2022 Adobe Systems Incorporated
3 | #
4 | # Licensed under the Apache License, Version 2.0 (the "License");
5 | # you may not use this file except in compliance with the License.
6 | # You may obtain a copy of the License at
7 | #
8 | # http://www.apache.org/licenses/LICENSE-2.0
9 | #
10 | # Unless required by applicable law or agreed to in writing, software
11 | # distributed under the License is distributed on an "AS IS" BASIS,
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | # See the License for the specific language governing permissions and
14 | # limitations under the License.
15 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 |
17 | name: Release
18 |
19 | # Run workflow on commits to default branch
20 | on: workflow_dispatch
21 |
22 | jobs:
23 | release:
24 | runs-on: ubuntu-latest
25 | steps:
26 | # Check out Git repository
27 | - uses: actions/checkout@v3
28 |
29 | # Set up environment with Java and Maven
30 | - name: Set up JDK
31 | uses: actions/setup-java@v1
32 | with:
33 | java-version: 11
34 |
35 | # Set up dependency cache
36 | - name: Cache local Maven repository
37 | uses: actions/cache@v3
38 | with:
39 | path: ~/.m2/repository
40 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
41 | restore-keys: |
42 | ${{ runner.os }}-maven-
43 |
44 | # Import GPG key from env variable into keychain
45 | - name: Import GPG key
46 | env:
47 | GPG_SECRET_KEYS: ${{ secrets.GPG_SECRET_KEYS }}
48 | GPG_OWNERTRUST: ${{ secrets.GPG_OWNERTRUST }}
49 | run: |
50 | echo $GPG_SECRET_KEYS | base64 --decode | gpg --import --no-tty --batch --yes
51 | echo $GPG_OWNERTRUST | base64 --decode | gpg --import-ownertrust --no-tty --batch --yes
52 |
53 | - name: Configure git user for release commits
54 | env:
55 | X_GITHUB_USERNAME: ${{ secrets.ADOBE_BOT_GITHUB_USERNAME }}
56 | run: |
57 | git config user.email "Grp-opensourceoffice@adobe.com"
58 | git config user.name "${X_GITHUB_USERNAME}"
59 |
60 | # Deploy to OSSRH, which will automatically release to Central Repository
61 | - name: Release to Central Repository
62 | env:
63 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
64 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
65 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
66 | X_GITHUB_USERNAME: ${{ secrets.ADOBE_BOT_GITHUB_USERNAME }}
67 | X_GITHUB_PASSWORD: ${{ secrets.ADOBE_BOT_GITHUB_PASSWORD }}
68 | run: mvn -B clean release:prepare release:perform -Prelease,ossrh,it-basic -s ./.github/workflows/settings.xml
69 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepage/clientlibs/js/remotepage.js:
--------------------------------------------------------------------------------
1 | /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 | ~ Copyright 2020 Adobe
3 | ~
4 | ~ Licensed under the Apache License, Version 2.0 (the "License");
5 | ~ you may not use this file except in compliance with the License.
6 | ~ You may obtain a copy of the License at
7 | ~
8 | ~ http://www.apache.org/licenses/LICENSE-2.0
9 | ~
10 | ~ Unless required by applicable law or agreed to in writing, software
11 | ~ distributed under the License is distributed on an "AS IS" BASIS,
12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | ~ See the License for the specific language governing permissions and
14 | ~ limitations under the License.
15 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
16 |
17 | (function(){
18 |
19 | const generateScriptLink = (path) => {
20 | const element = document.createElement('script');
21 | element.type = 'text/javascript'
22 | element.src = path;
23 |
24 | return element;
25 | };
26 |
27 | const generateStyleLink = (path) => {
28 | const element = document.createElement('link');
29 | element.type = 'text/css';
30 | element.rel = 'stylesheet';
31 | element.href = path;
32 |
33 | return element;
34 | };
35 |
36 | const sanitizeUrl = (url) => {
37 | let { pathname, origin } = new URL(url, location.origin); // defaults to own origin
38 | pathname = pathname.replace(/\/\//, '/');
39 | return `${origin}${pathname}`;
40 | };
41 |
42 | const domain = document.body.dataset.remoteUrl;
43 |
44 | if(domain) {
45 | const manifestUrl = sanitizeUrl(`${domain}/asset-manifest.json`);
46 | fetch(manifestUrl)
47 | .then(response => response.json())
48 | .then(asset => {
49 | const { entrypoints } = asset;
50 | let files = entrypoints?.client?.js || entrypoints;
51 |
52 | if (Array.isArray(files)) {
53 | const bodyFragment = document.createDocumentFragment();
54 | const headFragment = document.createDocumentFragment();
55 |
56 | files.forEach(item => {
57 | const filePath = sanitizeUrl(`${domain}/${item}`);
58 | if(item.indexOf('.css') > 0) {
59 | headFragment.appendChild(generateStyleLink(filePath));
60 | } else {
61 | bodyFragment.appendChild(generateScriptLink(filePath));
62 | }
63 | });
64 | headFragment.hasChildNodes() && document.head.appendChild(headFragment);
65 | bodyFragment.hasChildNodes() && document.body.appendChild(bodyFragment);
66 | }
67 | });
68 | }
69 | })();
70 |
--------------------------------------------------------------------------------
/core/src/test/java/com/adobe/aem/spa/project/core/internal/impl/utils/RequestUtilsTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | package com.adobe.aem.spa.project.core.internal.impl.utils;
14 |
15 | import org.apache.sling.api.SlingHttpServletRequest;
16 | import org.junit.jupiter.api.BeforeEach;
17 | import org.junit.jupiter.api.Test;
18 | import org.junit.jupiter.api.extension.ExtendWith;
19 | import org.mockito.Mock;
20 | import org.mockito.junit.jupiter.MockitoExtension;
21 | import org.mockito.junit.jupiter.MockitoSettings;
22 | import org.mockito.quality.Strictness;
23 |
24 | import com.day.cq.wcm.api.Page;
25 |
26 | import static com.adobe.aem.spa.project.core.internal.impl.utils.RequestUtils.getJsonExportURL;
27 | import static com.adobe.aem.spa.project.core.internal.impl.utils.RequestUtils.getURL;
28 | import static org.junit.jupiter.api.Assertions.assertEquals;
29 | import static org.junit.jupiter.api.Assertions.assertNull;
30 | import static org.mockito.Mockito.when;
31 |
32 | @ExtendWith(MockitoExtension.class)
33 | @MockitoSettings(strictness = Strictness.LENIENT)
34 | class RequestUtilsTest {
35 |
36 | // Mock paths
37 | private static final String CONTEXT_PATH = "/context/path";
38 |
39 | private static final String PAGE_PATH = "/path/to/page";
40 |
41 | @Mock
42 | private SlingHttpServletRequest request;
43 |
44 | @Mock
45 | private Page page;
46 |
47 | @BeforeEach
48 | void beforeEach() {
49 | when(request.getContextPath()).thenReturn(CONTEXT_PATH);
50 | when(page.getPath()).thenReturn(PAGE_PATH);
51 | }
52 |
53 | @Test
54 | void testGetURLWithoutVanityUrl() {
55 | // Without vanity URL
56 | String vanityUrl = "";
57 | when(page.getVanityUrl()).thenReturn(vanityUrl);
58 | assertEquals(CONTEXT_PATH + PAGE_PATH + ".html", getURL(request, page));
59 | }
60 |
61 | @Test
62 | void testGetURLWithVanityUrl() {
63 | // With vanity URL
64 | String vanityUrl = "/vanity/url";
65 | when(page.getVanityUrl()).thenReturn(vanityUrl);
66 | assertEquals(CONTEXT_PATH + vanityUrl, getURL(request, page));
67 | }
68 |
69 | @Test
70 | void testGetJsonExportURL() {
71 | assertNull(getJsonExportURL(""));
72 | assertEquals("some/path.model.json", getJsonExportURL("some/path"));
73 | assertEquals("path/with/some.model.json", getJsonExportURL("path/with/some.more.selectors.html"));
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepagenext/clientlibs/js/remotepagenext.js:
--------------------------------------------------------------------------------
1 | /*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2 | ~ Copyright 2020 Adobe
3 | ~
4 | ~ Licensed under the Apache License, Version 2.0 (the "License");
5 | ~ you may not use this file except in compliance with the License.
6 | ~ You may obtain a copy of the License at
7 | ~
8 | ~ http://www.apache.org/licenses/LICENSE-2.0
9 | ~
10 | ~ Unless required by applicable law or agreed to in writing, software
11 | ~ distributed under the License is distributed on an "AS IS" BASIS,
12 | ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | ~ See the License for the specific language governing permissions and
14 | ~ limitations under the License.
15 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
16 |
17 | (function(){
18 |
19 | const generateScriptLink = (path) => {
20 | const element = document.createElement('script');
21 | element.type = 'text/javascript'
22 | element.src = path;
23 |
24 | return element;
25 | };
26 |
27 | const generateStyleLink = (path) => {
28 | const element = document.createElement('link');
29 | element.type = 'text/css';
30 | element.rel = 'stylesheet';
31 | element.href = path;
32 |
33 | return element;
34 | };
35 |
36 | const loadScriptsAndStyles = (origin) => {
37 | return fetch(`${origin}/asset-manifest.json`)
38 | .then(response => response.json())
39 | .then(asset => {
40 | const { entrypoints } = asset;
41 | if (entrypoints && Array.isArray(entrypoints)) {
42 | const bodyFragment = document.createDocumentFragment();
43 | const headFragment = document.createDocumentFragment();
44 |
45 | entrypoints.forEach(item => {
46 | const filePath = (`${origin}/_next/${item}`);
47 | if(item.indexOf('.css') > 0) {
48 | headFragment.appendChild(generateStyleLink(filePath));
49 | } else {
50 | bodyFragment.appendChild(generateScriptLink(filePath));
51 | }
52 | });
53 | headFragment.hasChildNodes() && document.head.appendChild(headFragment);
54 | bodyFragment.hasChildNodes() && document.body.appendChild(bodyFragment);
55 | }
56 | });
57 | };
58 |
59 | const remoteUrl = document.body.dataset.remoteUrl;
60 |
61 | if(remoteUrl) {
62 | let { origin, pathname } = new URL(remoteUrl);
63 | // Remove trailing slash
64 | pathname = pathname.replace(/\/$/,'');
65 | fetch(`${origin}/api/getNextProps?path=${pathname}`)
66 | .then(res => res.json())
67 | .then(res => {
68 | document.getElementById("__NEXT_DATA__").textContent = JSON.stringify(res);
69 | })
70 | .then(() => loadScriptsAndStyles(origin));
71 | }
72 | })();
73 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/internal/impl/utils/RequestUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | package com.adobe.aem.spa.project.core.internal.impl.utils;
14 |
15 | import org.apache.commons.lang3.StringUtils;
16 | import org.apache.sling.api.SlingHttpServletRequest;
17 | import org.jetbrains.annotations.NotNull;
18 |
19 | import com.day.cq.wcm.api.Page;
20 |
21 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.JSON_EXPORT_SUFFIX;
22 |
23 | public class RequestUtils {
24 | private RequestUtils() {
25 | }
26 |
27 | /**
28 | * Given a {@link Page}, this method returns the correct URL, taking into account that the provided page might provide a vanity URL
29 | *
30 | * @param request The current request, used to determine the server's context path
31 | * @param page The page
32 | * @return The URL of the page identified by the provided path, or the original path if this doesn't identify a {@link Page}
33 | */
34 | @NotNull
35 | public static String getURL(@NotNull SlingHttpServletRequest request, @NotNull Page page) {
36 | String contextPath = request.getContextPath();
37 | String contextPathNotNull = contextPath == null ? "" : contextPath;
38 | String vanityURL = page.getVanityUrl();
39 | return StringUtils.isEmpty(vanityURL) ? contextPathNotNull + page.getPath() + ".html" : contextPathNotNull + vanityURL;
40 | }
41 |
42 | /**
43 | * Returns a model URL for the given page URL
44 | *
45 | * @param url Page URL
46 | * @return Model URL
47 | */
48 | public static String getJsonExportURL(@NotNull String url) {
49 | if (StringUtils.isBlank(url)) {
50 | return null;
51 | }
52 |
53 | int dotIndex = url.indexOf('.');
54 |
55 | if (dotIndex < 0) {
56 | dotIndex = url.length();
57 | }
58 |
59 | return url.substring(0, dotIndex) + JSON_EXPORT_SUFFIX;
60 | }
61 |
62 | /**
63 | * Returns a model URL for the given page URL
64 | *
65 | * @param slingRequest The current servlet request
66 | * @param page Page for which to get the model URL
67 | * @return Model URL
68 | */
69 | public static String getPageJsonExportUrl(@NotNull SlingHttpServletRequest slingRequest, @NotNull com.day.cq.wcm.api.Page page) {
70 | return RequestUtils.getJsonExportURL(RequestUtils.getURL(slingRequest, page));
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/internal/impl/RemotePageImpl.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 | package com.adobe.aem.spa.project.core.internal.impl;
13 |
14 | import com.adobe.aem.spa.project.core.models.RemotePage;
15 | import com.adobe.cq.export.json.ContainerExporter;
16 | import com.adobe.cq.export.json.ExporterConstants;
17 | import com.day.cq.commons.inherit.InheritanceValueMap;
18 | import com.day.cq.wcm.api.Page;
19 | import com.day.cq.commons.inherit.HierarchyNodeInheritanceValueMap;
20 | import com.day.cq.wcm.api.PageManager;
21 | import org.apache.sling.api.SlingHttpServletRequest;
22 | import org.apache.sling.models.annotations.DefaultInjectionStrategy;
23 | import org.apache.sling.models.annotations.Exporter;
24 | import org.apache.sling.models.annotations.Model;
25 | import org.apache.sling.models.annotations.injectorspecific.Self;
26 | import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
27 |
28 | /**
29 | * RemotePage model implementation - Page that allows rendering and editing in AEM of a
30 | * remote SPA that exists at the URL defined in the page properties
31 | */
32 | @Model(
33 | adaptables = SlingHttpServletRequest.class,
34 | adapters = { RemotePage.class, ContainerExporter.class },
35 | resourceType = { RemotePageImpl.RESOURCE_TYPE_SPA, RemotePageImpl.RESOURCE_TYPE_NEXT },
36 | defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
37 | )
38 | @Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
39 | public class RemotePageImpl extends PageImpl implements RemotePage {
40 |
41 | static final String RESOURCE_TYPE_SPA = "spa-project-core/components/remotepage";
42 | static final String RESOURCE_TYPE_NEXT = "spa-project-core/components/remotepagenext";
43 |
44 | @Self
45 | private SlingHttpServletRequest request;
46 |
47 | @ValueMapValue
48 | private String remoteSPAUrl;
49 |
50 | @Override
51 | public String getRemoteSPAUrl() {
52 | String spaUrl = "";
53 |
54 | if(remoteSPAUrl != null) {
55 | spaUrl = remoteSPAUrl;
56 | } else {
57 | PageManager pageManager = request.getResourceResolver().adaptTo(PageManager.class);
58 | Page page = pageManager.getContainingPage(request.getResource());
59 | if(page != null) {
60 | InheritanceValueMap pageProperties = new HierarchyNodeInheritanceValueMap(page.getContentResource());
61 | spaUrl = pageProperties.getInherited("remoteSPAUrl", "");
62 | }
63 | }
64 | return spaUrl;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/core/src/test/java/com/adobe/aem/spa/project/core/internal/impl/utils/StyleUtilsTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | package com.adobe.aem.spa.project.core.internal.impl.utils;
14 |
15 | import org.junit.jupiter.api.BeforeEach;
16 | import org.junit.jupiter.api.Test;
17 | import org.junit.jupiter.api.extension.ExtendWith;
18 | import org.mockito.Mock;
19 | import org.mockito.junit.jupiter.MockitoExtension;
20 | import org.mockito.junit.jupiter.MockitoSettings;
21 | import org.mockito.quality.Strictness;
22 |
23 | import com.adobe.aem.spa.project.core.internal.HierarchyConstants;
24 | import com.day.cq.wcm.api.designer.Style;
25 |
26 | import static org.junit.jupiter.api.Assertions.assertEquals;
27 | import static org.junit.jupiter.api.Assertions.assertFalse;
28 | import static org.junit.jupiter.api.Assertions.assertNull;
29 | import static org.junit.jupiter.api.Assertions.assertTrue;
30 | import static org.mockito.ArgumentMatchers.any;
31 | import static org.mockito.ArgumentMatchers.anyString;
32 | import static org.mockito.Mockito.when;
33 |
34 | @ExtendWith(MockitoExtension.class)
35 | @MockitoSettings(strictness = Strictness.LENIENT)
36 | class StyleUtilsTest {
37 |
38 | private static final String PN_STRUCTURE_DEPTH = "structureDepth";
39 |
40 | private static final int STRUCTURE_DEPTH = 12;
41 |
42 | @Mock
43 | private Style style;
44 |
45 | @Test
46 | void testGetPageTreeDepthStyleIsNull() {
47 | // No style
48 | assertEquals(0, StyleUtils.getPageTreeDepth(null, PN_STRUCTURE_DEPTH));
49 | }
50 |
51 | @Test
52 | void testGetPageTreeDepthStyleIsNotNull() {
53 | // structureDepth set to 12
54 | when(style.get(anyString(), any())).thenReturn(STRUCTURE_DEPTH);
55 | assertEquals(STRUCTURE_DEPTH, StyleUtils.getPageTreeDepth(style, PN_STRUCTURE_DEPTH));
56 | }
57 |
58 | @Test
59 | void testGetStructureDepthStyleIsNull() {
60 | // No style
61 | assertNull(StyleUtils.getStructureDepth(null, PN_STRUCTURE_DEPTH));
62 | }
63 |
64 | @Test
65 | void testGetStructureDepthStyleIsNotNull() {
66 | // structureDepth set to 12
67 | when(style.get(anyString(), any())).thenReturn(STRUCTURE_DEPTH);
68 | assertEquals(STRUCTURE_DEPTH, (int) StyleUtils.getStructureDepth(style, PN_STRUCTURE_DEPTH));
69 | }
70 |
71 | @Test
72 | void testIsRootPageAttributeMissingOrFalse() {
73 | // `PN_IS_ROOT` property is missing or set to `false`
74 | when(style.get(HierarchyConstants.PN_IS_ROOT, false)).thenReturn(false);
75 | assertFalse(StyleUtils.isRootPage(style));
76 | }
77 |
78 | @Test
79 | void testIsRootPageAttributeTrue() {
80 | // `PN_IS_ROOT` set to `true`
81 | when(style.get(HierarchyConstants.PN_IS_ROOT, false)).thenReturn(true);
82 | assertTrue(StyleUtils.isRootPage(style));
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Adobe Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language.
18 | * Being respectful of differing viewpoints and experiences.
19 | * Gracefully accepting constructive criticism.
20 | * Focusing on what is best for the community.
21 | * Showing empathy towards other community members.
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances.
27 | * Trolling, insulting/derogatory comments, and personal or political attacks.
28 | * Public or private harassment.
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission.
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting.
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [https://contributor-covenant.org/version/1/4][version].
72 |
73 | [homepage]: https://contributor-covenant.org
74 | [version]: https://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/models/Page.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | package com.adobe.aem.spa.project.core.models;
14 |
15 | import java.util.Map;
16 |
17 | import org.jetbrains.annotations.NotNull;
18 | import org.jetbrains.annotations.Nullable;
19 | import org.osgi.annotation.versioning.ConsumerType;
20 |
21 | import com.adobe.cq.export.json.ComponentExporter;
22 | import com.adobe.cq.export.json.ContainerExporter;
23 | import com.adobe.cq.export.json.hierarchy.HierarchyNodeExporter;
24 | import com.fasterxml.jackson.annotation.JsonIgnore;
25 |
26 | /**
27 | * Defines the {@code Page} Sling Model used for the {@code /apps/spa/project/core/models/page} component
28 | */
29 | @ConsumerType
30 | public interface Page extends com.adobe.cq.wcm.core.components.models.Page, HierarchyNodeExporter {
31 | /**
32 | * Key for the depth of the tree of pages that is to be exported
33 | */
34 | String PN_STRUCTURE_DEPTH = "structureDepth";
35 |
36 | /**
37 | * URL to the root model of the App
38 | *
39 | * @return {@link String}
40 | */
41 | @Nullable
42 | @JsonIgnore
43 | default String getHierarchyRootJsonExportUrl() {
44 | throw new UnsupportedOperationException();
45 | }
46 |
47 | /**
48 | * Root page model of the current hierarchy of pages
49 | *
50 | * @return {@link Page}
51 | */
52 | @Nullable
53 | @JsonIgnore
54 | default Page getHierarchyRootModel() {
55 | throw new UnsupportedOperationException();
56 | }
57 |
58 | /**
59 | * @see ContainerExporter#getExportedItemsOrder()
60 | */
61 | @NotNull
62 | @Override
63 | default String[] getExportedItemsOrder() {
64 | throw new UnsupportedOperationException();
65 | }
66 |
67 | /**
68 | * @see ContainerExporter#getExportedItems()
69 | */
70 | @NotNull
71 | @Override
72 | default Map getExportedItems() {
73 | throw new UnsupportedOperationException();
74 | }
75 |
76 | /**
77 | * @see ContainerExporter#getExportedType()
78 | */
79 | @NotNull
80 | @Override
81 | default String getExportedType() {
82 | throw new UnsupportedOperationException();
83 | }
84 |
85 | /**
86 | * @see HierarchyNodeExporter#getExportedHierarchyType
87 | */
88 | @Override
89 | default String getExportedHierarchyType() {
90 | throw new UnsupportedOperationException();
91 | }
92 |
93 | /**
94 | * @see HierarchyNodeExporter#getExportedPath
95 | */
96 | @Override
97 | default String getExportedPath() {
98 | throw new UnsupportedOperationException();
99 | }
100 |
101 | /**
102 | * @see HierarchyNodeExporter#getExportedChildren
103 | */
104 | @Override
105 | default Map getExportedChildren() {
106 | throw new UnsupportedOperationException();
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepagenext/_cq_dialog/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
24 |
28 |
29 |
34 |
35 |
39 |
40 |
43 |
44 |
48 |
49 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepage/_cq_dialog/.content.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
24 |
28 |
29 |
34 |
35 |
39 |
40 |
43 |
44 |
48 |
49 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/core/src/test/java/com/adobe/aem/spa/project/core/internal/impl/ComponentContextRequestWrapperTest.java:
--------------------------------------------------------------------------------
1 | /* ************************************************************************
2 | * ADOBE CONFIDENTIAL
3 | * ___________________
4 | *
5 | * Copyright 2022 Adobe
6 | * All Rights Reserved.
7 | *
8 | * NOTICE: All information contained herein is, and remains
9 | * the property of Adobe and its suppliers, if any. The intellectual
10 | * and technical concepts contained herein are proprietary to Adobe
11 | * and its suppliers and are protected by all applicable intellectual
12 | * property laws, including trade secret and copyright laws.
13 | * Dissemination of this information or reproduction of this material
14 | * is strictly forbidden unless prior written permission is obtained
15 | * from Adobe.
16 | **************************************************************************/
17 | package com.adobe.aem.spa.project.core.internal.impl;
18 |
19 | import org.apache.sling.api.SlingHttpServletRequest;
20 | import org.apache.sling.testing.mock.osgi.MockOsgi;
21 | import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
22 | import org.junit.jupiter.api.BeforeEach;
23 | import org.junit.jupiter.api.Test;
24 | import org.junit.jupiter.api.extension.ExtendWith;
25 | import org.mockito.Mock;
26 | import org.mockito.junit.jupiter.MockitoExtension;
27 |
28 | import com.day.cq.wcm.api.components.ComponentContext;
29 | import com.day.cq.wcm.commons.WCMUtils;
30 |
31 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.ATTR_COMPONENT_CONTEXT;
32 | import static org.junit.jupiter.api.Assertions.assertEquals;
33 | import static org.mockito.Mockito.mock;
34 |
35 | @ExtendWith(MockitoExtension.class)
36 | class ComponentContextRequestWrapperTest {
37 |
38 | @Mock
39 | private ComponentContext originalContext;
40 |
41 | @Mock
42 | private HierarchyComponentContextWrapper childContext;
43 |
44 | private SlingHttpServletRequest originalRequest;
45 |
46 | @BeforeEach
47 | void beforeEach() {
48 | originalRequest = new MockSlingHttpServletRequest(MockOsgi.newBundleContext());
49 | originalRequest.setAttribute(ATTR_COMPONENT_CONTEXT, originalContext);
50 | }
51 |
52 | @Test
53 | void testContextIsSavedOnWrap() {
54 | // when
55 | SlingHttpServletRequest childRequest = new ComponentContextRequestWrapper(originalRequest, childContext);
56 |
57 | // then
58 | assertEquals(childContext, WCMUtils.getComponentContext(childRequest));
59 | }
60 |
61 | @Test
62 | void testWrappingDoesNotChangeOriginal() {
63 | // when
64 | new ComponentContextRequestWrapper(originalRequest, childContext);
65 |
66 | // then
67 | assertEquals(originalContext, WCMUtils.getComponentContext(originalRequest));
68 | }
69 |
70 | @Test
71 | void testContextIsImmutableAndPassedToOriginal() {
72 | // having
73 | HierarchyComponentContextWrapper newContext = mock(HierarchyComponentContextWrapper.class);
74 | SlingHttpServletRequest childRequest = new ComponentContextRequestWrapper(originalRequest, childContext);
75 |
76 | // when
77 | childRequest.setAttribute(ATTR_COMPONENT_CONTEXT, newContext);
78 |
79 | // then
80 | assertEquals(childContext, WCMUtils.getComponentContext(childRequest));
81 | assertEquals(newContext, WCMUtils.getComponentContext(originalRequest));
82 | }
83 |
84 | @Test
85 | void testChangingOriginalDoesNotAffectWrapper() {
86 | // having
87 | HierarchyComponentContextWrapper newContext = mock(HierarchyComponentContextWrapper.class);
88 | SlingHttpServletRequest childRequest = new ComponentContextRequestWrapper(originalRequest, childContext);
89 |
90 | // when
91 | originalRequest.setAttribute(ATTR_COMPONENT_CONTEXT, newContext);
92 |
93 | // then
94 | assertEquals(childContext, WCMUtils.getComponentContext(childRequest));
95 | assertEquals(newContext, WCMUtils.getComponentContext(originalRequest));
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepage/README.md:
--------------------------------------------------------------------------------
1 |
12 | RemotePage
13 | ====
14 | Custom page component for editing remote React SPA within AEM. This component fetches all the necessary assets from the application's generated asset-manifest.json and uses this for rendering the SPA within AEM.
15 |
16 | ## Asset manifest and why is it used here?
17 | When `npm run build` is run in a create-react-app project, react-scripts uses the [webpack-manifest-plugin](https://www.npmjs.com/package/webpack-manifest-plugin) to output an `asset-manifest.json` file which will contain the paths of all generated assets. This file can be used by AEM to fetch the required scripts or styles for rendering the remote application.
18 | - By default, CRA will assume your application is hosted at the serving web server's root or a subpath as specified in package.json (homepage) and will hence ignore the hostname. This means that all the paths will be relative in the asset-manifest.json. If the assets need to be referenced verbatim to the url you provide (hostname included), you can use the `PUBLIC_URL` environment variable [as recommended by React](https://create-react-app.dev/docs/advanced-configuration/)
19 |
20 | ## Requirements
21 |
22 | ### Enable CORS in Developement
23 | Since AEM needs to fetch the asset-manifest of the SPA hosted on a different domain, we need to enable CORS in the application.To do this, React provides an option to [configure proxy manually](https://create-react-app.dev/docs/proxying-api-requests-in-development/#configuring-the-proxy-manually)
24 | and update the response header.
25 |
26 | A simple implementation is as follows-
27 | - Create `src/setupProxy.js`
28 | - Add the following content in it
29 | ```javascript
30 | module.exports = (app) => {
31 | app.use((req, res, next) => {
32 | res.header("Access-Control-Allow-Origin", AEM_INSTANCE_ORIGIN);
33 | next();
34 | });
35 | };
36 | ```
37 | where `AEM_INSTANCE_ORIGIN` is the origin of the AEM instance in which the remote SPA will be edited(_eg: http://localhost:4502_). To allow all origins, you can give "*" as the value as well.
38 |
39 |
40 | ### Configure remote URL
41 | The remote application's URL can be set via the Page Properties, which is then written to JCR for this RemotePage component. The property defined for this is `./remoteSPAUrl`
42 | The URL to be provided is the location at which the asset-manifest exists. In most cases, this would be the host URL of the application.
43 | For eg: for a remote react application at `https://test.com` with the generated asset manifest at `https://test.com/asset-manifest.json` the URL to be provided would be `https://test.com`.
44 |
45 | ### Render the SPA in AEM
46 | By default, the remotepage component creates an element -
47 |
48 | ``
49 |
50 | which will behave as the root DOM node in which the react application is rendered.
51 | If the remote application to be edited has a different root DOM element/id, please override the [body.html](./body.html) in the extended component.
52 |
53 | ## Limitations
54 | - Current implementation supports remote React applications only.
55 | - Internal css defined in the application's root html file as well as inline css on the root DOM node will not be available when doing remote rendering in AEM. However all external style sheets will be available as expected, as well as all styles within the React application.
56 | - Navigating and refreshing the page, within AEM Editor, to a page that does not exist on AEM site does not work. The result will be a 404.
57 |
--------------------------------------------------------------------------------
/ui.apps/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 |
6 |
7 |
8 |
9 | com.adobe.aem
10 | spa.project.core
11 | 1.3.19-SNAPSHOT
12 | ../pom.xml
13 |
14 |
15 |
16 |
17 |
18 | spa.project.core.ui.apps
19 | content-package
20 | spa-project-core - UI apps
21 | UI apps package for spa-project-core
22 |
23 |
24 |
25 |
26 |
27 | src/main/content/jcr_root
28 |
29 |
30 |
31 |
32 |
33 | org.apache.jackrabbit
34 | filevault-package-maven-plugin
35 | true
36 |
37 | com.adobe.aem
38 | spa.project.core.ui.apps
39 | application
40 | merge
41 |
42 | none
43 |
44 |
45 |
46 |
47 | com.day.jcr.vault
48 | content-package-maven-plugin
49 | true
50 |
51 | true
52 | true
53 |
54 |
55 |
56 |
57 | org.apache.sling
58 | htl-maven-plugin
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | com.adobe.cq
69 | core.wcm.components.all
70 | zip
71 |
72 |
73 |
74 | com.adobe.aem
75 | uber-jar
76 | apis
77 |
78 |
79 |
80 | javax.jcr
81 | jcr
82 |
83 |
84 |
85 | javax.servlet
86 | javax.servlet-api
87 |
88 |
89 |
90 | com.day.cq.wcm
91 | cq-wcm-taglib
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/internal/impl/HierarchyComponentContextWrapper.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | package com.adobe.aem.spa.project.core.internal.impl;
14 |
15 | import java.util.Set;
16 |
17 | import org.apache.sling.api.resource.Resource;
18 |
19 | import com.day.cq.wcm.api.Page;
20 | import com.day.cq.wcm.api.components.AnalyzeContext;
21 | import com.day.cq.wcm.api.components.Component;
22 | import com.day.cq.wcm.api.components.ComponentContext;
23 | import com.day.cq.wcm.api.components.EditContext;
24 | import com.day.cq.wcm.api.designer.Cell;
25 |
26 | /**
27 | * Implementation of the {@link ComponentContext} that allows to pass {@link Page} and {@link ComponentContext} as constructor parameters.
28 | *
29 | * Provides a solution to save a static reference to any {@link Page} in order to define the page for a request.
30 | */
31 | public class HierarchyComponentContextWrapper implements ComponentContext {
32 |
33 | private ComponentContext wrappedComponentContext;
34 |
35 | private Page hierarchyPage;
36 |
37 | public HierarchyComponentContextWrapper(ComponentContext wrappedComponentContext, Page hierarchyPage) {
38 | this.wrappedComponentContext = wrappedComponentContext;
39 | this.hierarchyPage = hierarchyPage;
40 | }
41 |
42 | @Override
43 | public ComponentContext getParent() {
44 | return this.wrappedComponentContext.getParent();
45 | }
46 |
47 | @Override
48 | public ComponentContext getRoot() {
49 | return this.wrappedComponentContext.getRoot();
50 | }
51 |
52 | @Override
53 | public boolean isRoot() {
54 | return this.wrappedComponentContext.isRoot();
55 | }
56 |
57 | @Override
58 | public Resource getResource() {
59 | return this.wrappedComponentContext.getResource();
60 | }
61 |
62 | @Override
63 | public Cell getCell() {
64 | return this.wrappedComponentContext.getCell();
65 | }
66 |
67 | @Override
68 | public EditContext getEditContext() {
69 | return this.wrappedComponentContext.getEditContext();
70 | }
71 |
72 | @Override
73 | public AnalyzeContext getAnalyzeContext() {
74 | return this.wrappedComponentContext.getAnalyzeContext();
75 | }
76 |
77 | @Override
78 | public Component getComponent() {
79 | return this.wrappedComponentContext.getComponent();
80 | }
81 |
82 | @Override
83 | public Page getPage() {
84 | return hierarchyPage;
85 | }
86 |
87 | @Override
88 | public Object getAttribute(String s) {
89 | return this.wrappedComponentContext.getAttribute(s);
90 | }
91 |
92 | @Override
93 | public Object setAttribute(String s, Object o) {
94 | return this.wrappedComponentContext.setAttribute(s, o);
95 | }
96 |
97 | @Override
98 | public Set getCssClassNames() {
99 | return this.wrappedComponentContext.getCssClassNames();
100 | }
101 |
102 | @Override
103 | public boolean hasDecoration() {
104 | return this.wrappedComponentContext.hasDecoration();
105 | }
106 |
107 | @Override
108 | public void setDecorate(boolean b) {
109 | this.wrappedComponentContext.setDecorate(b);
110 | }
111 |
112 | @Override
113 | public String getDecorationTagName() {
114 | return this.wrappedComponentContext.getDecorationTagName();
115 | }
116 |
117 | @Override
118 | public void setDecorationTagName(String s) {
119 | this.wrappedComponentContext.setDecorationTagName(s);
120 | }
121 |
122 | @Override
123 | public String getDefaultDecorationTagName() {
124 | return this.wrappedComponentContext.getDefaultDecorationTagName();
125 | }
126 |
127 | @Override
128 | public void setDefaultDecorationTagName(String s) {
129 | this.wrappedComponentContext.setDecorationTagName(s);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/core/src/test/java/com/adobe/aem/spa/project/core/internal/impl/HierarchyComponentContextWrapperTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | package com.adobe.aem.spa.project.core.internal.impl;
14 |
15 | import java.util.Collections;
16 |
17 | import org.apache.sling.api.resource.Resource;
18 | import org.junit.jupiter.api.BeforeEach;
19 | import org.junit.jupiter.api.Test;
20 | import org.junit.jupiter.api.extension.ExtendWith;
21 | import org.mockito.Mock;
22 | import org.mockito.junit.jupiter.MockitoExtension;
23 | import org.mockito.junit.jupiter.MockitoSettings;
24 | import org.mockito.quality.Strictness;
25 |
26 | import com.day.cq.wcm.api.components.AnalyzeContext;
27 | import com.day.cq.wcm.api.components.Component;
28 | import com.day.cq.wcm.api.components.ComponentContext;
29 | import com.day.cq.wcm.api.components.EditContext;
30 | import com.day.cq.wcm.api.designer.Cell;
31 |
32 | import static org.junit.jupiter.api.Assertions.assertEquals;
33 | import static org.mockito.ArgumentMatchers.anyBoolean;
34 | import static org.mockito.ArgumentMatchers.anyObject;
35 | import static org.mockito.ArgumentMatchers.anyString;
36 | import static org.mockito.Mockito.mock;
37 | import static org.mockito.Mockito.spy;
38 | import static org.mockito.Mockito.times;
39 | import static org.mockito.Mockito.verify;
40 | import static org.mockito.Mockito.when;
41 |
42 | @ExtendWith(MockitoExtension.class)
43 | @MockitoSettings(strictness = Strictness.LENIENT)
44 | class HierarchyComponentContextWrapperTest {
45 |
46 | @Mock
47 | private ComponentContext componentContext;
48 |
49 | private com.day.cq.wcm.api.Page page = spy(com.day.cq.wcm.api.Page.class);
50 |
51 | private HierarchyComponentContextWrapper hccw;
52 |
53 | private ComponentContext getMockedComponentContext() {
54 | when(componentContext.getParent()).thenReturn(mock(ComponentContext.class));
55 | when(componentContext.getRoot()).thenReturn(mock(ComponentContext.class));
56 | when(componentContext.isRoot()).thenReturn(Boolean.TRUE);
57 | when(componentContext.getResource()).thenReturn(mock(Resource.class));
58 | when(componentContext.getCell()).thenReturn(mock(Cell.class));
59 | when(componentContext.getEditContext()).thenReturn(mock(EditContext.class));
60 | when(componentContext.getAnalyzeContext()).thenReturn(mock(AnalyzeContext.class));
61 | when(componentContext.getComponent()).thenReturn(mock(Component.class));
62 | when(componentContext.getAttribute(anyString())).thenReturn(mock(Object.class));
63 | when(componentContext.getCssClassNames()).thenReturn(Collections.EMPTY_SET);
64 | when(componentContext.hasDecoration()).thenReturn(Boolean.TRUE);
65 | when(componentContext.getDecorationTagName()).thenReturn("decorationTagName");
66 | when(componentContext.getDefaultDecorationTagName()).thenReturn("defaultDecorationTagName");
67 |
68 | return componentContext;
69 | }
70 |
71 | @BeforeEach
72 | void beforeEach() {
73 | componentContext = getMockedComponentContext();
74 | hccw = new HierarchyComponentContextWrapper(componentContext, page);
75 | }
76 |
77 | @Test
78 | void testGetters() {
79 | assertEquals(componentContext.getParent(), hccw.getParent());
80 | assertEquals(componentContext.getRoot(), hccw.getRoot());
81 | assertEquals(componentContext.isRoot(), hccw.isRoot());
82 | assertEquals(componentContext.getResource(), hccw.getResource());
83 | assertEquals(componentContext.getCell(), hccw.getCell());
84 | assertEquals(componentContext.getEditContext(), hccw.getEditContext());
85 | assertEquals(componentContext.getAnalyzeContext(), hccw.getAnalyzeContext());
86 | assertEquals(componentContext.getComponent(), hccw.getComponent());
87 | assertEquals(page, hccw.getPage());
88 | assertEquals(componentContext.getAttribute("1"), hccw.getAttribute("1"));
89 | assertEquals(componentContext.getCssClassNames(), hccw.getCssClassNames());
90 | assertEquals(componentContext.hasDecoration(), hccw.hasDecoration());
91 | assertEquals(componentContext.getDecorationTagName(), hccw.getDecorationTagName());
92 | assertEquals(componentContext.getDefaultDecorationTagName(), hccw.getDefaultDecorationTagName());
93 | }
94 |
95 | @Test
96 | void testSetDecorationTagName() {
97 | verify(componentContext, times(0)).setDecorationTagName(anyString());
98 | hccw.setDecorationTagName("decorationTagName");
99 | verify(componentContext, times(1)).setDecorationTagName(anyString());
100 | }
101 |
102 | @Test
103 | void testSetAttribute() {
104 | verify(componentContext, times(0)).setAttribute(anyString(), anyObject());
105 | hccw.setAttribute("attributeName", "attributeValue");
106 | verify(componentContext, times(1)).setAttribute(anyString(), anyObject());
107 | }
108 |
109 | @Test
110 | void testSetDecorate() {
111 | verify(componentContext, times(0)).setDecorate(anyBoolean());
112 | hccw.setDecorate(Boolean.TRUE);
113 | verify(componentContext, times(1)).setDecorate(anyBoolean());
114 | }
115 |
116 | @Test
117 | void testSetDefaultDecorationTagName() {
118 | verify(componentContext, times(0)).setDecorationTagName(anyString());
119 | hccw.setDefaultDecorationTagName("defaultDecorationTagName");
120 | verify(componentContext, times(1)).setDecorationTagName(anyString());
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepagenext/README.md:
--------------------------------------------------------------------------------
1 |
12 | RemotePageNext
13 | ====
14 | Custom page component for editing remote Next.js SPA within AEM. This component fetches all the necessary assets from the application's generated asset-manifest.json and uses this for rendering the SPA within AEM.
15 |
16 |
17 | ## Requirements
18 |
19 | ### Create and expose an asset-manifest.json file
20 | In a React app, running `npm run build` generates an `asset-manifest.json` file using the [webpack-manifest-plugin](https://www.npmjs.com/package/webpack-manifest-plugin) which will contain the paths of all generated assets.
21 | Using a similar approach, we need to add the necessary configs in the Next.js app to output a similar asset-manifest.json file. This file can then be used by AEM to fetch the required scripts or styles for rendering the remote application.
22 | Here we are using the [next-assets-manifest](https://www.npmjs.com/package/next-assets-manifest) package to generate the asset-manifest file in the Next.js app and transforming it to have a similar structure as the React version.
23 |
24 | - `npm install next.config.js`
25 | - If not available already, create a new file `next.config.js` in the project root.
26 | - In this, add the following snippet.
27 |
28 | ```javascript
29 | const withAssetsManifest = require('next-assets-manifest');
30 |
31 | module.exports = withAssetsManifest({
32 | assetsManifest: {
33 | output: "../public/asset-manifest.json",
34 | transform: (assets, manifest) => {
35 | const entrypoints = [];
36 | for (let file in assets) {
37 | if(assets[file].endsWith(".js") || assets[file].endsWith(".css")) {
38 | entrypoints.push(assets[file]);
39 | }
40 | }
41 | return {
42 | files: assets,
43 | entrypoints: entrypoints
44 | };
45 | }
46 | }
47 | });
48 | ```
49 |
50 | ### Create an endpoint to fetch the app's initial data
51 | A Next.js app does initial data population for a page using data props set in the page's DOM. For remotely loading the app inside AEM, we need to fetch this data and set it in the AEM RemotePageNext component.
52 | - ```npm install xmldom ```
53 | - Create `/pages/api/getNextProps.js`
54 | - Add the following snippet in it:
55 |
56 | ```javascript
57 | const { DOMParser } = require('xmldom');
58 | const { NEXT_PUBLIC_URL} = process.env;
59 |
60 | export default function handler(req, res) {
61 | let { path } = req.query;
62 |
63 | fetch(NEXT_PUBLIC_URL + (path || ''))
64 | .then(t => t.text())
65 | .then(t => {
66 | const parser = new DOMParser();
67 | const doc = parser.parseFromString(t, 'text/html');
68 | const data = doc.getElementById("__NEXT_DATA__").textContent;
69 | res.status(200).json(data);
70 | });
71 | }
72 | ```
73 | where `NEXT_PUBLIC_URL` is the origin of the Next.js app to be edited.
74 | _Note: Please ensure CORS configuration as detailed in the next step has been done to allow the AEM instance to be able to fetch this data._
75 |
76 | ### Enable CORS in Developement
77 | Since AEM needs to fetch the data props of the Next.js app hosted on a different domain, we need to enable CORS in the application.This can be done adding the necessary response header using the `next.config.js` file
78 |
79 | A simple implementation is as follows-
80 | - Add the following content in the `next.config.js` file created during the [asset-manifest step](#create-and-expose-an-asset-manifest.json-file)
81 |
82 | ```javascript
83 | const { NEXT_PUBLIC_AEM_HOST_URI } = process.env;
84 |
85 | module.exports = withAssetsManifest({
86 | async headers() {
87 | return [
88 | {
89 | source: '/api/getNextProps',
90 | headers: [{
91 | key: 'Access-Control-Allow-Origin',
92 | value: NEXT_PUBLIC_AEM_HOST_URI
93 | }],
94 | },
95 | ]
96 | }
97 | ...
98 | })
99 | ```
100 | where `NEXT_PUBLIC_AEM_HOST_URI` is the origin of the AEM instance in which the remote SPA will be edited(_eg: http://localhost:4502_). To allow all origins, you can give "*" as the value as well.
101 |
102 |
103 | ### Configure remote URL
104 | The remote application's URL can be set via the Page Properties, which is then written to JCR for this RemotePageNext component. The property defined for this is `./remoteSPAUrl`
105 | The URL to be provided is the URL of the specific Next.js page to be edited. In most cases, this would be the host URL of the application.
106 | For eg: for editing a remote Next.js page at `https://test.com/abc`, this same URL needs to be provided in AEM as well.
107 |
108 | ### Displaying images in AEM
109 | When using the [Image](https://nextjs.org/docs/api-reference/next/image) component provided by `next/image`, the image `src` is usually provided as a relative path to the image within the project.To load the image in AEM, the URL needs to be absolute. For this we can use the `loader` prop provided by `next/image`.
110 |
111 | A sample implementation of an Image component using absolute URL is as follows -
112 |
113 | ```javascript
114 | import Image from 'next/image';
115 | const { NEXT_PUBLIC_URL } = process.env;
116 |
117 | const myLoader = ({ src, width, quality }) => {
118 | return `${NEXT_PUBLIC_URL}${src}?w=${width}&q=${quality || 75}`
119 | }
120 |
121 | export default function Test({...}) {
122 | return (
123 |
129 | );
130 | }
131 | ```
132 | where `NEXT_PUBLIC_URL` is the origin of the Next.js app being edited.
133 |
--------------------------------------------------------------------------------
/all/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 |
4 |
5 |
6 |
7 |
8 | com.adobe.aem
9 | spa.project.core
10 | 1.3.19-SNAPSHOT
11 | ../pom.xml
12 |
13 |
14 |
15 |
16 |
17 | spa.project.core.all
18 | content-package
19 | spa-project-core - All
20 | All content package for spa-project-core
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | org.apache.jackrabbit
32 | filevault-package-maven-plugin
33 | true
34 |
35 | spa-project-core
36 |
37 |
38 | com.adobe.aem
39 | spa.project.core.core
40 | /apps/spa-project-core-bundles/install
41 |
42 |
43 |
44 |
45 | com.adobe.aem
46 | spa.project.core.ui.apps
47 | true
48 |
49 |
50 |
51 |
52 |
53 | com.day.jcr.vault
54 | content-package-maven-plugin
55 | true
56 |
57 | true
58 | true
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | autoInstallSinglePackage
70 |
71 | false
72 |
73 |
74 |
75 |
76 | com.day.jcr.vault
77 | content-package-maven-plugin
78 |
79 |
80 | install-package
81 |
82 | install
83 |
84 |
85 | http://${aem.host}:${aem.port}/crx/packmgr/service.jsp
86 | true
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | autoInstallSinglePackagePublish
96 |
97 | false
98 |
99 |
100 |
101 |
102 | com.day.jcr.vault
103 | content-package-maven-plugin
104 |
105 |
106 | install-package-publish
107 |
108 | install
109 |
110 |
111 | http://${aem.publish.host}:${aem.publish.port}/crx/packmgr/service.jsp
112 | true
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 | com.adobe.aem
128 | spa.project.core.core
129 | 1.3.19-SNAPSHOT
130 |
131 |
132 | com.adobe.aem
133 | spa.project.core.ui.apps
134 | 1.3.19-SNAPSHOT
135 | zip
136 |
137 |
138 |
139 | com.adobe.cq
140 | core.wcm.components.all
141 | zip
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/core/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 4.0.0
4 |
5 | com.adobe.aem
6 | spa.project.core
7 | 1.3.19-SNAPSHOT
8 | ../pom.xml
9 |
10 | spa.project.core.core
11 | spa-project-core - Core
12 | Core bundle for spa-project-core
13 |
14 |
15 |
16 | org.apache.sling
17 | sling-maven-plugin
18 |
19 |
20 | biz.aQute.bnd
21 | bnd-maven-plugin
22 |
23 |
24 | bnd-process
25 |
26 | bnd-process
27 |
28 |
29 |
30 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | biz.aQute.bnd
44 | bnd-baseline-maven-plugin
45 |
46 | false
47 |
48 |
49 |
50 | baseline
51 |
52 | baseline
53 |
54 |
55 |
56 |
57 |
58 | org.apache.maven.plugins
59 | maven-surefire-plugin
60 |
61 |
62 | org.apache.maven.plugins
63 | maven-jar-plugin
64 |
65 |
66 | ${project.build.outputDirectory}/META-INF/MANIFEST.MF
67 |
68 |
69 |
70 |
71 | org.jacoco
72 | jacoco-maven-plugin
73 | 0.8.5
74 |
75 |
76 |
77 | prepare-agent
78 |
79 |
80 |
81 | report
82 | test
83 |
84 | report
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | org.jetbrains
95 | annotations
96 |
97 |
98 | javax.inject
99 | javax.inject
100 |
101 |
102 |
103 |
104 | org.osgi
105 | org.osgi.annotation.versioning
106 |
107 |
108 | org.osgi
109 | org.osgi.annotation.bundle
110 |
111 |
112 | org.osgi
113 | org.osgi.service.metatype.annotations
114 |
115 |
116 | org.osgi
117 | org.osgi.service.component.annotations
118 |
119 |
120 | org.osgi
121 | org.osgi.service.component
122 |
123 |
124 | org.osgi
125 | org.osgi.service.cm
126 |
127 |
128 | org.osgi
129 | org.osgi.service.event
130 |
131 |
132 | org.osgi
133 | org.osgi.service.log
134 |
135 |
136 | org.osgi
137 | org.osgi.framework
138 |
139 |
140 | org.osgi
141 | org.osgi.resource
142 |
143 |
144 |
145 | org.slf4j
146 | slf4j-api
147 |
148 |
149 | javax.jcr
150 | jcr
151 |
152 |
153 | javax.servlet
154 | javax.servlet-api
155 |
156 |
157 | com.adobe.aem
158 | uber-jar
159 | apis
160 |
161 |
162 | com.adobe.cq
163 | core.wcm.components.core
164 |
165 |
166 | org.apache.sling
167 | org.apache.sling.models.api
168 |
169 |
170 | org.junit.jupiter
171 | junit-jupiter
172 | test
173 |
174 |
175 | org.mockito
176 | mockito-core
177 | test
178 |
179 |
180 | org.mockito
181 | mockito-junit-jupiter
182 | test
183 |
184 |
185 | junit-addons
186 | junit-addons
187 |
188 |
189 | io.wcm
190 | io.wcm.testing.aem-mock.junit5
191 |
192 |
193 | uk.org.lidalia
194 | slf4j-test
195 |
196 |
197 |
198 |
--------------------------------------------------------------------------------
/core/src/test/java/com/adobe/aem/spa/project/core/internal/impl/PageImplTest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | package com.adobe.aem.spa.project.core.internal.impl;
14 |
15 | import java.util.HashMap;
16 | import java.util.Map;
17 |
18 | import org.apache.sling.api.SlingHttpServletRequest;
19 | import org.apache.sling.api.request.RequestParameter;
20 | import org.apache.sling.models.factory.ModelFactory;
21 | import org.junit.jupiter.api.BeforeEach;
22 | import org.junit.jupiter.api.Test;
23 | import org.junit.jupiter.api.extension.ExtendWith;
24 | import org.mockito.InjectMocks;
25 | import org.mockito.Mock;
26 | import org.mockito.junit.jupiter.MockitoExtension;
27 | import org.mockito.junit.jupiter.MockitoSettings;
28 | import org.mockito.quality.Strictness;
29 |
30 | import com.adobe.aem.spa.project.core.internal.HierarchyConstants;
31 | import com.adobe.aem.spa.project.core.models.Page;
32 | import com.adobe.cq.export.json.hierarchy.type.HierarchyTypes;
33 | import com.day.cq.wcm.api.designer.Style;
34 |
35 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.JSON_EXPORT_SUFFIX;
36 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.PN_STRUCTURE_PATTERNS;
37 | import static org.junit.jupiter.api.Assertions.assertEquals;
38 | import static org.junit.jupiter.api.Assertions.assertTrue;
39 | import static org.mockito.ArgumentMatchers.any;
40 | import static org.mockito.ArgumentMatchers.eq;
41 | import static org.mockito.Mockito.mock;
42 | import static org.mockito.Mockito.times;
43 | import static org.mockito.Mockito.verify;
44 | import static org.mockito.Mockito.when;
45 |
46 | @ExtendWith(MockitoExtension.class)
47 | @MockitoSettings(strictness = Strictness.LENIENT)
48 | class PageImplTest {
49 |
50 | private static final String CURRENT_PAGE_PATH = "/path/to/current/page";
51 |
52 | @Mock
53 | private com.day.cq.wcm.api.Page currentPage;
54 |
55 | @Mock
56 | private Style currentStyle;
57 |
58 | @Mock
59 | private ModelFactory modelFactory;
60 |
61 | @Mock
62 | private SlingHttpServletRequest request;
63 |
64 | @Mock
65 | private com.adobe.cq.wcm.core.components.models.Page delegate;
66 |
67 | @InjectMocks
68 | private PageImpl page;
69 |
70 | @BeforeEach
71 | void beforeEach() {
72 | // Current page
73 | when(currentPage.getPath()).thenReturn(CURRENT_PAGE_PATH);
74 |
75 | // Current style
76 | when(currentStyle.get(HierarchyConstants.PN_IS_ROOT, false)).thenReturn(false);
77 |
78 | // Root page
79 | page.setRootPage(currentPage);
80 |
81 | // Request
82 | RequestParameter requestParameter = mock(RequestParameter.class);
83 | when(request.getRequestParameter(eq(PN_STRUCTURE_PATTERNS.toLowerCase()))).thenReturn(requestParameter);
84 |
85 | // Mock delegated methods where required to prevent errors
86 | when(delegate.getExportedItemsOrder()).thenReturn(new String[] {});
87 | when(delegate.getExportedType()).thenReturn("some/resource/type");
88 | }
89 |
90 | @Test
91 | void testGetExportedHierarchyType() {
92 | assertEquals(HierarchyTypes.PAGE, page.getExportedHierarchyType());
93 | }
94 |
95 | @Test
96 | void testGetExportedChildrenModelsIsNotNull() {
97 | // descendedPageModels is not null
98 | Map descendedPageModels = new HashMap<>();
99 | page.setDescendedPageModels(descendedPageModels);
100 | assertEquals(descendedPageModels, page.getExportedChildren());
101 | }
102 |
103 | @Test
104 | void testGetExportedChildrenModelsIsNull() {
105 | // descendedPageModels is null
106 | assertTrue(page.getExportedChildren().isEmpty());
107 | }
108 |
109 | @Test
110 | void testGetExportedPath() {
111 | assertEquals(CURRENT_PAGE_PATH, page.getExportedPath());
112 | }
113 |
114 | @Test
115 | void testGetHierarchyRootJsonExportUrlIsNotRoot() {
116 | // Is not root page
117 | assertEquals(CURRENT_PAGE_PATH + JSON_EXPORT_SUFFIX, page.getHierarchyRootJsonExportUrl());
118 | }
119 |
120 | @Test
121 | void testGetHierarchyRootJsonExportUrlIsRoot() {
122 | // Is root page
123 | when(currentStyle.get(HierarchyConstants.PN_IS_ROOT, false)).thenReturn(true);
124 | assertEquals(CURRENT_PAGE_PATH + JSON_EXPORT_SUFFIX, page.getHierarchyRootJsonExportUrl());
125 | }
126 |
127 | @Test
128 | void testGetHierarchyRootModelIsNotRoot() {
129 | // Is not root page
130 | when(modelFactory.getModelFromWrappedRequest(any(), any(), any())).thenReturn(page);
131 | assertEquals(page, page.getHierarchyRootModel());
132 | }
133 |
134 | @Test
135 | void testGetHierarchyRootModelIsRoot() {
136 | // Is root page
137 | when(currentStyle.get(HierarchyConstants.PN_IS_ROOT, false)).thenReturn(true);
138 | assertEquals(page, page.getHierarchyRootModel());
139 | }
140 |
141 | // Test delegations
142 | @Test
143 | void testGetLanguage() {
144 | page.getLanguage();
145 | verify(delegate, times(1)).getLanguage();
146 | }
147 |
148 | @Test
149 | void testGetLastModifiedDate() {
150 | page.getLastModifiedDate();
151 | verify(delegate, times(1)).getLastModifiedDate();
152 | }
153 |
154 | @Test
155 | void testGetKeywords() {
156 | page.getKeywords();
157 | verify(delegate, times(1)).getKeywords();
158 | }
159 |
160 | @Test
161 | void testGetDesignPath() {
162 | page.getDesignPath();
163 | verify(delegate, times(1)).getDesignPath();
164 | }
165 |
166 | @Test
167 | void testGetStaticDesignPath() {
168 | page.getStaticDesignPath();
169 | verify(delegate, times(1)).getStaticDesignPath();
170 | }
171 |
172 | @Test
173 | void testGetTitle() {
174 | page.getTitle();
175 | verify(delegate, times(1)).getTitle();
176 | }
177 |
178 | @Test
179 | void testGetTemplateName() {
180 | page.getTemplateName();
181 | verify(delegate, times(1)).getTemplateName();
182 | }
183 |
184 | @Test
185 | void testGetClientLibCategories() {
186 | page.getClientLibCategories();
187 | verify(delegate, times(1)).getClientLibCategories();
188 | }
189 |
190 | @Test
191 | void testGetExportedType() {
192 | page.getExportedType();
193 | verify(delegate, times(1)).getExportedType();
194 | }
195 |
196 | @Test
197 | void testGetMainContentSelector() {
198 | page.getMainContentSelector();
199 | verify(delegate, times(1)).getMainContentSelector();
200 | }
201 |
202 | @Test
203 | void testGetClientLibCategoriesJsBody() {
204 | page.getClientLibCategoriesJsBody();
205 | verify(delegate, times(1)).getClientLibCategoriesJsBody();
206 | }
207 |
208 | @Test
209 | void testGetClientLibCategoriesJsHead() {
210 | page.getClientLibCategoriesJsBody();
211 | verify(delegate, times(1)).getClientLibCategoriesJsBody();
212 | }
213 |
214 | @Test
215 | void testGetAppResourcesPath() {
216 | page.getAppResourcesPath();
217 | verify(delegate, times(1)).getAppResourcesPath();
218 | }
219 |
220 | @Test
221 | void testGetCssClassNames() {
222 | page.getCssClassNames();
223 | verify(delegate, times(1)).getCssClassNames();
224 | }
225 |
226 | @Test
227 | void testGetExportedItemsOrder() {
228 | page.getExportedItemsOrder();
229 | verify(delegate, times(1)).getExportedItemsOrder();
230 | }
231 |
232 | @Test
233 | void testGetExportedItems() {
234 | page.getExportedItems();
235 | verify(delegate, times(1)).getExportedItems();
236 | }
237 |
238 | @Test
239 | void testGetRedirectTarget() {
240 | page.getRedirectTarget();
241 | verify(delegate, times(1)).getRedirectTarget();
242 | }
243 |
244 | @Test
245 | void testHasCloudconfigSupport() {
246 | page.hasCloudconfigSupport();
247 | verify(delegate, times(1)).hasCloudconfigSupport();
248 | }
249 |
250 | @Test
251 | void testGetComponentsResourceTypes() {
252 | page.getComponentsResourceTypes();
253 | verify(delegate, times(1)).getComponentsResourceTypes();
254 | }
255 |
256 | @Test
257 | void testGetBrandSlug() {
258 | page.getBrandSlug();
259 | verify(delegate, times(1)).getBrandSlug();
260 | }
261 |
262 | @Test
263 | void testHtmlPageItems() {
264 | page.getHtmlPageItems();
265 | verify(delegate, times(1)).getHtmlPageItems();
266 | }
267 |
268 | }
269 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/internal/impl/PageImpl.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | package com.adobe.aem.spa.project.core.internal.impl;
14 |
15 | import java.util.Calendar;
16 | import java.util.List;
17 | import java.util.Map;
18 |
19 | import java.util.Set;
20 | import javax.inject.Inject;
21 |
22 | import org.apache.sling.api.SlingHttpServletRequest;
23 | import org.apache.sling.api.resource.Resource;
24 | import org.apache.sling.models.annotations.Exporter;
25 | import org.apache.sling.models.annotations.Model;
26 | import org.apache.sling.models.annotations.Via;
27 | import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
28 | import org.apache.sling.models.annotations.injectorspecific.ScriptVariable;
29 | import org.apache.sling.models.annotations.injectorspecific.Self;
30 | import org.apache.sling.models.annotations.via.ResourceSuperType;
31 | import org.apache.sling.models.factory.ModelFactory;
32 | import org.jetbrains.annotations.NotNull;
33 | import org.jetbrains.annotations.Nullable;
34 |
35 | import com.adobe.aem.spa.project.core.internal.impl.utils.HierarchyUtils;
36 | import com.adobe.aem.spa.project.core.internal.impl.utils.RequestUtils;
37 | import com.adobe.aem.spa.project.core.internal.impl.utils.StyleUtils;
38 | import com.adobe.aem.spa.project.core.models.Page;
39 | import com.adobe.cq.export.json.ComponentExporter;
40 | import com.adobe.cq.export.json.ContainerExporter;
41 | import com.adobe.cq.export.json.ExporterConstants;
42 | import com.adobe.cq.export.json.hierarchy.type.HierarchyTypes;
43 | import com.adobe.cq.wcm.core.components.models.HtmlPageItem;
44 | import com.adobe.cq.wcm.core.components.models.NavigationItem;
45 | import com.day.cq.wcm.api.designer.Style;
46 | import com.fasterxml.jackson.annotation.JsonIgnore;
47 |
48 | /**
49 | * Page that allows the retrieval of the model in JSON format with hierarchical structures of more than one page.
50 | * The content of the JSON export of the page's model is limited by two parameters (see {@link HierarchyUtils}):
51 | *
52 | *
filterPatterns - paths from which Pages are to be included
53 | *
traversalDepth - number of levels to be included
54 | *
55 | * However, there is also the possibility to use the Java API to get the model. If the {@link #getHierarchyRootModel()} function is used,
56 | * the {@link HierarchyUtils#createHierarchyServletRequest(SlingHttpServletRequest, com.day.cq.wcm.api.Page, com.day.cq.wcm.api.Page)}
57 | * function would wrap the request saving
58 | *
59 | *
The original request
60 | *
The root page of the hierarchy
61 | *
The entry point page - so the page for which the actual request was made
62 | *
63 | * in order to provide the full hierarchy from the root, with all descendants' models of the root page (with respect to filterPatterns and
64 | * traversalDepth) plus the entry point page (even if was excluded based on rules enforced by filterPatterns or traversalDepth).
65 | * Among other information, the exported structure would contain:
66 | *
67 | *
A flat map of all descendants' models identifiable by their paths ({@link PageImpl#getExportedChildren()} to :children)
68 | *
A map of the content of the page ({@link PageImpl#getExportedItems()} to :items), together with the order
69 | * ({@link PageImpl#getExportedItemsOrder()} to :itemsOrder
70 | *
71 | */
72 | @Model(adaptables = SlingHttpServletRequest.class, adapters = { Page.class,
73 | ContainerExporter.class }, resourceType = PageImpl.RESOURCE_TYPE)
74 | @Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
75 | public class PageImpl implements Page {
76 |
77 | static final String RESOURCE_TYPE = "spa-project-core/components/page";
78 |
79 | // Delegated to Page v1
80 | @ScriptVariable
81 | @Via(type = ResourceSuperType.class)
82 | private com.day.cq.wcm.api.Page currentPage;
83 |
84 | // Delegated to Page v1
85 | @ScriptVariable(injectionStrategy = InjectionStrategy.OPTIONAL)
86 | @JsonIgnore
87 | @Via(type = ResourceSuperType.class)
88 | private Style currentStyle;
89 |
90 | // Delegated to Page v1
91 | @Inject
92 | private ModelFactory modelFactory;
93 |
94 | // Delegated to Page v2
95 | @Self
96 | @Via(type = ResourceSuperType.class)
97 | private SlingHttpServletRequest request;
98 |
99 | @ScriptVariable
100 | private Resource resource;
101 |
102 | // "delegate" object with which methods from Page v1/v2 can be used
103 | @Self
104 | @Via(type = ResourceSuperType.class)
105 | protected com.adobe.cq.wcm.core.components.models.Page delegate;
106 |
107 | /**
108 | * {@link Map} containing the page models with their corresponding paths (as keys)
109 | */
110 | private Map descendedPageModels;
111 |
112 | private com.day.cq.wcm.api.Page rootPage;
113 |
114 | /**
115 | * Package-private setter for descendedPageModels (required for tests)
116 | */
117 | void setDescendedPageModels(Map descendedPageModels) {
118 | this.descendedPageModels = descendedPageModels;
119 | }
120 |
121 | /**
122 | * Package-private setter for rootPage (required for tests)
123 | */
124 | void setRootPage(com.day.cq.wcm.api.Page rootPage) {
125 | this.rootPage = rootPage;
126 | }
127 |
128 | @Nullable
129 | @Override
130 | public String getExportedHierarchyType() {
131 | return HierarchyTypes.PAGE;
132 | }
133 |
134 | @NotNull
135 | @Override
136 | public Map getExportedChildren() {
137 | if (descendedPageModels == null) {
138 | setDescendedPageModels(HierarchyUtils.getDescendantsModels(request, currentPage, currentStyle, modelFactory));
139 | }
140 |
141 | return descendedPageModels;
142 | }
143 |
144 | @NotNull
145 | @Override
146 | public String getExportedPath() {
147 | return currentPage.getPath();
148 | }
149 |
150 | @Nullable
151 | @Override
152 | public String getHierarchyRootJsonExportUrl() {
153 | if (isRootPage()) {
154 | return RequestUtils.getPageJsonExportUrl(request, currentPage);
155 | }
156 |
157 | if (rootPage == null) {
158 | setRootPage(HierarchyUtils.getRootPage(resource, currentPage));
159 | }
160 |
161 | if (rootPage != null) {
162 | return RequestUtils.getPageJsonExportUrl(request, rootPage);
163 | }
164 | return null;
165 | }
166 |
167 | /**
168 | * Returns the model of the root page which this page is a part of
169 | *
170 | * @return Root page model
171 | */
172 | @Nullable
173 | @Override
174 | public Page getHierarchyRootModel() {
175 | if (isRootPage()) {
176 | return this;
177 | }
178 |
179 | if (rootPage == null) {
180 | setRootPage(HierarchyUtils.getRootPage(resource, currentPage));
181 | }
182 |
183 | if (rootPage == null) {
184 | return null;
185 | }
186 |
187 | return modelFactory.getModelFromWrappedRequest(request, rootPage.getContentResource(), this.getClass());
188 | }
189 |
190 | private boolean isRootPage() {
191 | return currentStyle != null && StyleUtils.isRootPage(currentStyle);
192 | }
193 |
194 | // Delegated to Page v1
195 |
196 | @Override
197 | public String getLanguage() {
198 | return delegate.getLanguage();
199 | }
200 |
201 | // Delegated to Page v1
202 | @Override
203 | public Calendar getLastModifiedDate() {
204 | return delegate.getLastModifiedDate();
205 | }
206 |
207 | // Delegated to Page v1
208 | @Override
209 | @JsonIgnore
210 | public String[] getKeywords() {
211 | return delegate.getKeywords();
212 | }
213 |
214 | // Delegated to Page v1
215 | @Override
216 | public String getDesignPath() {
217 | return delegate.getDesignPath();
218 | }
219 |
220 | // Delegated to Page v1
221 | @Override
222 | public String getStaticDesignPath() {
223 | return delegate.getStaticDesignPath();
224 | }
225 |
226 | // Delegated to Page v1
227 | @Override
228 | public String getTitle() {
229 | return delegate.getTitle();
230 | }
231 |
232 | // Delegated to Page v1
233 | @Override
234 | public String getTemplateName() {
235 | return delegate.getTemplateName();
236 | }
237 |
238 | // Delegated to Page v1
239 | @Override
240 | @JsonIgnore
241 | public String[] getClientLibCategories() {
242 | return delegate.getClientLibCategories();
243 | }
244 |
245 | // Delegated to Page v1
246 | @NotNull
247 | @Override
248 | public String getExportedType() {
249 | return delegate.getExportedType();
250 | }
251 |
252 | // Delegated to Page v2
253 | @Nullable
254 | @Override
255 | public String getMainContentSelector() {
256 | return delegate.getMainContentSelector();
257 | }
258 |
259 | // Delegated to Page v2
260 | @Override
261 | @JsonIgnore
262 | public String[] getClientLibCategoriesJsBody() {
263 | return delegate.getClientLibCategoriesJsBody();
264 | }
265 |
266 | // Delegated to Page v2
267 | @Override
268 | @JsonIgnore
269 | public String[] getClientLibCategoriesJsHead() {
270 | return delegate.getClientLibCategoriesJsHead();
271 | }
272 |
273 | // Delegated to Page v2
274 | @Override
275 | public String getAppResourcesPath() {
276 | return delegate.getAppResourcesPath();
277 | }
278 |
279 | // Delegated to Page v2
280 | @Override
281 | public String getCssClassNames() {
282 | return delegate.getCssClassNames();
283 | }
284 |
285 | // Delegated to Page v2
286 | @NotNull
287 | @Override
288 | public String[] getExportedItemsOrder() {
289 | return delegate.getExportedItemsOrder();
290 | }
291 |
292 | // Delegated to Page v2
293 | @NotNull
294 | @Override
295 | public Map getExportedItems() {
296 | return delegate.getExportedItems();
297 | }
298 |
299 | // Delegated to Page v2
300 | @Nullable
301 | @Override
302 | public NavigationItem getRedirectTarget() {
303 | return delegate.getRedirectTarget();
304 | }
305 |
306 | // Delegated to Page v2
307 | @Override
308 | public boolean hasCloudconfigSupport() {
309 | return delegate.hasCloudconfigSupport();
310 | }
311 |
312 | @NotNull
313 | @Override
314 | public Set getComponentsResourceTypes() {
315 | return delegate.getComponentsResourceTypes();
316 | }
317 |
318 | @Override
319 | public String getBrandSlug() {
320 | return delegate.getBrandSlug();
321 | }
322 |
323 | @Nullable
324 | @Override
325 | public List getHtmlPageItems() {
326 | return delegate.getHtmlPageItems();
327 | }
328 | }
329 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2020 Adobe
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/core/src/main/java/com/adobe/aem/spa/project/core/internal/impl/utils/HierarchyUtils.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Adobe. All rights reserved.
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4 | * you may not use this file except in compliance with the License. You may obtain a copy
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software distributed under
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9 | * OF ANY KIND, either express or implied. See the License for the specific language
10 | * governing permissions and limitations under the License.
11 | */
12 |
13 | package com.adobe.aem.spa.project.core.internal.impl.utils;
14 |
15 | import java.util.ArrayList;
16 | import java.util.Collections;
17 | import java.util.Iterator;
18 | import java.util.LinkedHashMap;
19 | import java.util.List;
20 | import java.util.Map;
21 | import java.util.regex.Pattern;
22 |
23 | import org.apache.commons.lang3.StringUtils;
24 | import org.apache.sling.api.SlingHttpServletRequest;
25 | import org.apache.sling.api.request.RequestParameter;
26 | import org.apache.sling.api.resource.Resource;
27 | import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
28 | import org.apache.sling.models.factory.ModelFactory;
29 | import org.jetbrains.annotations.NotNull;
30 | import org.jetbrains.annotations.Nullable;
31 | import org.slf4j.Logger;
32 | import org.slf4j.LoggerFactory;
33 |
34 | import com.adobe.aem.spa.project.core.internal.HierarchyConstants;
35 | import com.adobe.aem.spa.project.core.internal.impl.ComponentContextRequestWrapper;
36 | import com.adobe.aem.spa.project.core.internal.impl.HierarchyComponentContextWrapper;
37 | import com.adobe.aem.spa.project.core.models.Page;
38 | import com.day.cq.wcm.api.Template;
39 | import com.day.cq.wcm.api.TemplatedResource;
40 | import com.day.cq.wcm.api.components.ComponentContext;
41 | import com.day.cq.wcm.api.designer.Style;
42 | import com.day.cq.wcm.api.policies.ContentPolicy;
43 | import com.day.cq.wcm.api.policies.ContentPolicyManager;
44 | import com.day.cq.wcm.commons.WCMUtils;
45 |
46 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.ATTR_COMPONENT_CONTEXT;
47 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.ATTR_CURRENT_PAGE;
48 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.ATTR_HIERARCHY_ENTRY_POINT_PAGE;
49 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.ATTR_IS_CHILD_PAGE;
50 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.PN_IS_ROOT;
51 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.PN_STRUCTURE_PATTERNS;
52 | import static com.adobe.aem.spa.project.core.models.Page.PN_STRUCTURE_DEPTH;
53 |
54 | public class HierarchyUtils {
55 |
56 | private static final Logger LOGGER = LoggerFactory.getLogger(HierarchyUtils.class);
57 |
58 | private HierarchyUtils() {
59 | }
60 |
61 | /**
62 | * Creates a new request wrapping the {@code request} from the parameters.
63 | * The new request is created to set the
64 | *
65 | *
componentcontext - includes the {@link Page} from the {@code page} parameter and the context from {@code request}
66 | *
currentPage - {@link Page} from {@code page} parameter
67 | *
entryPointPage - {@link Page} from {@code entryPage} parameter
68 | *
69 | * attributes in order to ensure that the references to the page are accurate for the hierarchical structure.
70 | *
71 | * @param request Request to be wrapped
72 | * @param page Page to be referenced as statically containing the current page content
73 | * @param entryPage Page that is the entry point of the request
74 | * @return A {@link SlingHttpServletRequestWrapper} containing the given page and request
75 | */
76 | @Deprecated
77 | public static SlingHttpServletRequest createHierarchyServletRequest(@NotNull SlingHttpServletRequest request,
78 | @NotNull com.day.cq.wcm.api.Page page, @Nullable com.day.cq.wcm.api.Page entryPage) {
79 | SlingHttpServletRequest wrapperRequest = new SlingHttpServletRequestWrapper(request);
80 |
81 | ComponentContext componentContext = (ComponentContext) request.getAttribute(ATTR_COMPONENT_CONTEXT);
82 |
83 | // When traversing child pages, the currentPage must be updated
84 | HierarchyComponentContextWrapper componentContextWrapper = new HierarchyComponentContextWrapper(componentContext, page);
85 | wrapperRequest.setAttribute(ATTR_COMPONENT_CONTEXT, componentContextWrapper);
86 | wrapperRequest.setAttribute(ATTR_CURRENT_PAGE, page);
87 | wrapperRequest.setAttribute(ATTR_HIERARCHY_ENTRY_POINT_PAGE, entryPage);
88 |
89 | return wrapperRequest;
90 | }
91 |
92 | /**
93 | * Returns the request's entry point attribute value
94 | *
95 | * @param request Request to get the entry point attribute from
96 | * @return Entry point attribute value
97 | */
98 | public static com.day.cq.wcm.api.Page getEntryPoint(@NotNull SlingHttpServletRequest request) {
99 | return (com.day.cq.wcm.api.Page) request.getAttribute(ATTR_HIERARCHY_ENTRY_POINT_PAGE);
100 | }
101 |
102 | /**
103 | * Optionally adds a page that is the entry point of a site model request, even when was not added because of the root structure
104 | * configuration
105 | *
106 | * @param request Request
107 | * @param currentPage Current style
108 | * @param descendedPages List of descendants
109 | */
110 | public static void addEntryPointPage(SlingHttpServletRequest request, com.day.cq.wcm.api.Page currentPage,
111 | @NotNull List descendedPages) {
112 | // Child pages are only added to the root page
113 | if (Boolean.TRUE.equals(request.getAttribute(ATTR_IS_CHILD_PAGE))) {
114 | return;
115 | }
116 |
117 | com.day.cq.wcm.api.Page entryPointPage = HierarchyUtils.getEntryPoint(request);
118 |
119 | if (entryPointPage == null) {
120 | return;
121 | }
122 |
123 | // Filter the root page
124 | if (entryPointPage.getPath().equals(currentPage.getPath())) {
125 | return;
126 | }
127 |
128 | // Filter duplicates
129 | if (descendedPages.contains(entryPointPage)) {
130 | return;
131 | }
132 |
133 | descendedPages.add(entryPointPage);
134 | }
135 |
136 | /**
137 | * Returns the page structure patterns to filter the descendants to be exported. The patterns can either be stored on the template
138 | * policy of the page or provided as a request parameter
139 | *
140 | * @param request Request
141 | * @param currentStyle Current style
142 | * @return List of page structure patterns
143 | */
144 | @NotNull
145 | public static List getStructurePatterns(@NotNull SlingHttpServletRequest request, Style currentStyle) {
146 | RequestParameter pageFilterParameter = request.getRequestParameter(PN_STRUCTURE_PATTERNS.toLowerCase());
147 |
148 | String rawPageFilters = null;
149 |
150 | if (pageFilterParameter != null) {
151 | rawPageFilters = pageFilterParameter.getString();
152 | }
153 |
154 | if (currentStyle != null && StringUtils.isBlank(rawPageFilters)) {
155 | rawPageFilters = currentStyle.get(PN_STRUCTURE_PATTERNS, String.class);
156 | }
157 |
158 | if (StringUtils.isBlank(rawPageFilters)) {
159 | return Collections.emptyList();
160 | }
161 |
162 | String[] pageFilters = rawPageFilters.split(",");
163 |
164 | List pageFilterPatterns = new ArrayList<>();
165 | for (String pageFilter : pageFilters) {
166 | pageFilterPatterns.add(Pattern.compile(pageFilter));
167 | }
168 |
169 | return pageFilterPatterns;
170 | }
171 |
172 | /**
173 | * Returns the root page which the current page is part of
174 | *
175 | * @param resource Resource
176 | * @param currentPage Current page
177 | * @return Root page
178 | */
179 | public static com.day.cq.wcm.api.Page getRootPage(Resource resource, com.day.cq.wcm.api.Page currentPage) {
180 | com.day.cq.wcm.api.Page tempRootPage = currentPage;
181 |
182 | ContentPolicyManager contentPolicyManager = resource.getResourceResolver().adaptTo(ContentPolicyManager.class);
183 | if (contentPolicyManager == null) {
184 | LOGGER.error("Error determining SPA root page: Cannot adapt resource resolver to ContentPolicyManager class");
185 | return null;
186 | }
187 |
188 | while (tempRootPage != null) {
189 | Template template = tempRootPage.getTemplate();
190 | if (template != null && template.hasStructureSupport()) {
191 | Resource pageContentResource = tempRootPage.getContentResource();
192 | if (pageContentResource != null) {
193 | ContentPolicy contentPolicy = contentPolicyManager.getPolicy(pageContentResource);
194 | if (ContentPolicyUtils.propertyIsTrue(contentPolicy, PN_IS_ROOT)) {
195 | // Is the root page to return it
196 | LOGGER.debug("Found SPA root page: {}", tempRootPage.getPath());
197 | return tempRootPage;
198 | }
199 | }
200 | }
201 |
202 | // Is not the root page to move up the tree
203 | tempRootPage = tempRootPage.getParent();
204 | }
205 |
206 | // Root page not found
207 | LOGGER.error("SPA root page not found, returning null");
208 | return null;
209 | }
210 |
211 | /**
212 | * Traverses the tree of descendants of the page. Descendants that
213 | *
214 | *
Are not deeper than the defined depth
215 | *
Have a path that matches one of defined structurePattern