├── maven-settings.xml ├── .gitattributes ├── ui.apps ├── src │ └── main │ │ └── content │ │ ├── jcr_root │ │ └── apps │ │ │ ├── spa-project-core │ │ │ ├── components │ │ │ │ ├── remotepage │ │ │ │ │ ├── clientlibs │ │ │ │ │ │ ├── js.txt │ │ │ │ │ │ ├── .content.xml │ │ │ │ │ │ └── js │ │ │ │ │ │ │ └── remotepage.js │ │ │ │ │ ├── body.html │ │ │ │ │ ├── .content.xml │ │ │ │ │ ├── remotepage.html │ │ │ │ │ ├── _cq_dialog │ │ │ │ │ │ └── .content.xml │ │ │ │ │ └── README.md │ │ │ │ ├── remotepagenext │ │ │ │ │ ├── clientlibs │ │ │ │ │ │ ├── js.txt │ │ │ │ │ │ ├── .content.xml │ │ │ │ │ │ └── js │ │ │ │ │ │ │ └── remotepagenext.js │ │ │ │ │ ├── body.html │ │ │ │ │ ├── .content.xml │ │ │ │ │ ├── remotepagenext.html │ │ │ │ │ ├── _cq_dialog │ │ │ │ │ │ └── .content.xml │ │ │ │ │ └── README.md │ │ │ │ ├── page │ │ │ │ │ ├── body.html │ │ │ │ │ ├── .content.xml │ │ │ │ │ ├── page.html │ │ │ │ │ └── _cq_design_dialog │ │ │ │ │ │ └── .content.xml │ │ │ │ ├── .content.xml │ │ │ │ └── xf-page │ │ │ │ │ └── .content.xml │ │ │ └── .content.xml │ │ │ └── .content.xml │ │ └── META-INF │ │ └── vault │ │ └── filter.xml └── pom.xml ├── .gitignore ├── .editorconfig ├── all ├── src │ └── main │ │ └── content │ │ └── META-INF │ │ └── vault │ │ └── filter.xml └── pom.xml ├── DEV_GUIDELINES.md ├── .github ├── workflows │ ├── security.yml │ ├── ci.yml │ ├── settings.xml │ └── maven-release.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── core ├── src │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── adobe │ │ │ └── aem │ │ │ └── spa │ │ │ └── project │ │ │ └── core │ │ │ ├── internal │ │ │ ├── impl │ │ │ │ ├── ExperienceFragmentPageExporter.java │ │ │ │ ├── ExperienceFragmentPageImpl.java │ │ │ │ ├── ComponentContextRequestWrapper.java │ │ │ │ ├── utils │ │ │ │ │ ├── ContentPolicyUtils.java │ │ │ │ │ ├── StyleUtils.java │ │ │ │ │ ├── RequestUtils.java │ │ │ │ │ └── HierarchyUtils.java │ │ │ │ ├── RemotePageImpl.java │ │ │ │ ├── HierarchyComponentContextWrapper.java │ │ │ │ └── PageImpl.java │ │ │ └── HierarchyConstants.java │ │ │ └── models │ │ │ ├── package-info.java │ │ │ ├── RemotePage.java │ │ │ └── Page.java │ └── test │ │ └── java │ │ └── com │ │ └── adobe │ │ └── aem │ │ └── spa │ │ └── project │ │ └── core │ │ └── internal │ │ └── impl │ │ ├── RemotePageImplTest.java │ │ ├── utils │ │ ├── ContentPolicyUtilsTest.java │ │ ├── RequestUtilsTest.java │ │ ├── StyleUtilsTest.java │ │ └── HierarchyUtilsTest.java │ │ ├── ComponentContextRequestWrapperTest.java │ │ ├── HierarchyComponentContextWrapperTest.java │ │ └── PageImplTest.java └── pom.xml ├── README.md ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── LICENSE └── pom.xml /maven-settings.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepage/clientlibs/js.txt: -------------------------------------------------------------------------------- 1 | #base=js 2 | 3 | remotepage.js 4 | -------------------------------------------------------------------------------- /ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/remotepagenext/clientlibs/js.txt: -------------------------------------------------------------------------------- 1 | #base=js 2 | 3 | remotepagenext.js 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Maven 5 | target/ 6 | 7 | # Eclipse 8 | .classpath 9 | .project 10 | .settings/ 11 | 12 | # IntelliJ 13 | .idea/ 14 | *.iml 15 | -------------------------------------------------------------------------------- /ui.apps/src/main/content/jcr_root/apps/spa-project-core/components/page/body.html: -------------------------------------------------------------------------------- 1 | 2 |
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 | [![License](https://img.shields.io/badge/license-Apache%202-blue)](https://github.com/adobe/aem-spa-project-core/blob/master/LICENSE) 4 | [![Version](https://img.shields.io/maven-metadata/v/https/oss.sonatype.org/service/local/repositories/releases/content/com/adobe/aem/spa.project.core.all/maven-metadata.xml.svg?label=Version)](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 | {name} 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
  • 216 | *
217 | * will be returned in a flat list 218 | * 219 | * @param page Page from which to extract descended pages 220 | * @param slingRequest Request 221 | * @param structurePatterns Patterns to filter descended pages 222 | * @param depth Depth of the traversal 223 | * @return Flat list of matching descendants 224 | */ 225 | @NotNull 226 | public static List getDescendants(com.day.cq.wcm.api.Page page, SlingHttpServletRequest slingRequest, 227 | List structurePatterns, int depth) { 228 | // By default the depth is 0 meaning we do not expose descendants 229 | // If the value is set as a positive number it is going to be exposed until the counter is brought down to 0 230 | // If the value is set to a negative value all descendants will be exposed (full traversal tree - aka infinity) 231 | // Descendants pages do not expose their child pages 232 | if (page == null || depth == 0 || Boolean.TRUE.equals(slingRequest.getAttribute(HierarchyConstants.ATTR_IS_CHILD_PAGE))) { 233 | return new ArrayList<>(); 234 | } 235 | 236 | List pages = new ArrayList<>(); 237 | Iterator childPagesIterator = page.listChildren(); 238 | 239 | if (childPagesIterator == null || !childPagesIterator.hasNext()) { 240 | return new ArrayList<>(); 241 | } 242 | 243 | // we are about to explore one lower level down the tree 244 | depth--; 245 | 246 | boolean noPageFilters = structurePatterns.isEmpty(); 247 | 248 | while (childPagesIterator.hasNext()) { 249 | com.day.cq.wcm.api.Page childPage = childPagesIterator.next(); 250 | boolean found = noPageFilters; 251 | 252 | for (Pattern pageFilterPattern : structurePatterns) { 253 | if (pageFilterPattern.matcher(childPage.getPath()).find()) { 254 | found = true; 255 | break; 256 | } 257 | } 258 | 259 | if (!found) { 260 | continue; 261 | } 262 | 263 | pages.add(childPage); 264 | 265 | pages.addAll(getDescendants(childPage, slingRequest, structurePatterns, depth)); 266 | } 267 | 268 | return pages; 269 | } 270 | 271 | @Nullable 272 | protected static Page getDescendantModel(com.day.cq.wcm.api.Page childPage, SlingHttpServletRequest request, 273 | ModelFactory modelFactory) { 274 | Resource childPageContentResource = childPage.getContentResource(); 275 | 276 | if (childPageContentResource == null) { 277 | return null; 278 | } 279 | 280 | // Try to pass the templated content resource 281 | TemplatedResource templatedResource = childPageContentResource.adaptTo(TemplatedResource.class); 282 | 283 | if (templatedResource != null) { 284 | childPageContentResource = templatedResource; 285 | } 286 | 287 | HierarchyComponentContextWrapper componentContextWrapper = 288 | new HierarchyComponentContextWrapper(WCMUtils.getComponentContext(request), childPage); 289 | final SlingHttpServletRequest wrapperRequest = new ComponentContextRequestWrapper(request, componentContextWrapper); 290 | 291 | return modelFactory.getModelFromWrappedRequest(wrapperRequest, childPageContentResource, Page.class); 292 | } 293 | 294 | /** 295 | * Returns all descended page models of the currentPage plus the entryPoint page (even if was excluded based on rules enforced by 296 | * filterPatterns or traversalDepth) 297 | * 298 | * @param request Request 299 | * @param currentPage Current page 300 | * @param currentStyle Current style 301 | * @param modelFactory Model factory 302 | * @return {@link Map} containing the page models with their corresponding paths (as keys) 303 | */ 304 | @NotNull 305 | public static Map getDescendantsModels(SlingHttpServletRequest request, com.day.cq.wcm.api.Page currentPage, 306 | Style currentStyle, ModelFactory modelFactory) { 307 | int pageTreeTraversalDepth = StyleUtils.getPageTreeDepth(currentStyle, PN_STRUCTURE_DEPTH); 308 | 309 | List pageFilterPatterns = HierarchyUtils.getStructurePatterns(request, currentStyle); 310 | 311 | // Setting the child page to true to prevent child pages to expose their own child pages 312 | Map itemWrappers = new LinkedHashMap<>(); 313 | List descendants = HierarchyUtils.getDescendants(currentPage, request, pageFilterPatterns, 314 | pageTreeTraversalDepth); 315 | 316 | HierarchyUtils.addEntryPointPage(request, currentPage, descendants); 317 | 318 | // Add a flag to inform the model of the descendant page that it is not the root of the returned hierarchy 319 | request.setAttribute(HierarchyConstants.ATTR_IS_CHILD_PAGE, true); 320 | 321 | for (com.day.cq.wcm.api.Page childPage : descendants) { 322 | Page descendantModel = getDescendantModel(childPage, request, modelFactory); 323 | if (descendantModel != null) { 324 | itemWrappers.put(childPage.getPath(), descendantModel); 325 | } 326 | } 327 | 328 | return itemWrappers; 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /core/src/test/java/com/adobe/aem/spa/project/core/internal/impl/utils/HierarchyUtilsTest.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.Arrays; 17 | import java.util.HashMap; 18 | import java.util.Iterator; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.regex.Pattern; 22 | 23 | import org.apache.sling.api.SlingHttpServletRequest; 24 | import org.apache.sling.api.request.RequestParameter; 25 | import org.apache.sling.api.resource.Resource; 26 | import org.apache.sling.api.resource.ResourceResolver; 27 | import org.apache.sling.api.resource.ValueMap; 28 | import org.apache.sling.models.factory.ModelFactory; 29 | import org.apache.sling.testing.mock.osgi.MockOsgi; 30 | import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest; 31 | import org.junit.jupiter.api.BeforeEach; 32 | import org.junit.jupiter.api.DisplayName; 33 | import org.junit.jupiter.api.Nested; 34 | import org.junit.jupiter.api.Test; 35 | import org.junit.jupiter.api.extension.ExtendWith; 36 | import org.mockito.Mock; 37 | import org.mockito.junit.jupiter.MockitoExtension; 38 | import org.mockito.junit.jupiter.MockitoSettings; 39 | import org.mockito.quality.Strictness; 40 | import org.mockito.stubbing.Answer; 41 | 42 | import com.adobe.aem.spa.project.core.internal.HierarchyConstants; 43 | import com.day.cq.wcm.api.Page; 44 | import com.day.cq.wcm.api.Template; 45 | import com.day.cq.wcm.api.TemplatedResource; 46 | import com.day.cq.wcm.api.components.ComponentContext; 47 | import com.day.cq.wcm.api.designer.Style; 48 | import com.day.cq.wcm.api.policies.ContentPolicy; 49 | import com.day.cq.wcm.api.policies.ContentPolicyManager; 50 | import com.day.cq.wcm.commons.WCMUtils; 51 | 52 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.ATTR_COMPONENT_CONTEXT; 53 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.ATTR_CURRENT_PAGE; 54 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.ATTR_HIERARCHY_ENTRY_POINT_PAGE; 55 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.PN_IS_ROOT; 56 | import static com.adobe.aem.spa.project.core.internal.HierarchyConstants.PN_STRUCTURE_PATTERNS; 57 | import static com.adobe.aem.spa.project.core.models.Page.PN_STRUCTURE_DEPTH; 58 | import static org.apache.commons.collections.IteratorUtils.emptyIterator; 59 | import static org.junit.jupiter.api.Assertions.assertEquals; 60 | import static org.junit.jupiter.api.Assertions.assertNotNull; 61 | import static org.junit.jupiter.api.Assertions.assertNull; 62 | import static org.junit.jupiter.api.Assertions.assertTrue; 63 | import static org.mockito.ArgumentMatchers.any; 64 | import static org.mockito.ArgumentMatchers.eq; 65 | import static org.mockito.Mockito.mock; 66 | import static org.mockito.Mockito.spy; 67 | import static org.mockito.Mockito.times; 68 | import static org.mockito.Mockito.verify; 69 | import static org.mockito.Mockito.when; 70 | 71 | @ExtendWith(MockitoExtension.class) 72 | @MockitoSettings(strictness = Strictness.LENIENT) 73 | class HierarchyUtilsTest { 74 | private static final int DEPTH = 5; 75 | 76 | @Mock 77 | private Page childPage; 78 | 79 | @Mock 80 | private com.adobe.aem.spa.project.core.models.Page childPageModel; 81 | 82 | @Mock 83 | private Resource childPageContentResource; 84 | 85 | @Mock 86 | private Page currentPage; 87 | 88 | @Mock 89 | private Page entryPage; 90 | 91 | @Mock 92 | private ModelFactory modelFactory; 93 | 94 | @Mock 95 | private Page page; 96 | 97 | private SlingHttpServletRequest request; 98 | 99 | @Mock 100 | private RequestParameter requestParameter; 101 | 102 | @Mock 103 | private SlingHttpServletRequest requestWrapper; 104 | 105 | @Mock 106 | private Resource resource; 107 | 108 | private final List structurePatterns = new ArrayList<>(); 109 | 110 | @Mock 111 | private TemplatedResource templatedResource; 112 | 113 | @BeforeEach 114 | void beforeEach() { 115 | // Pages 116 | when(currentPage.getPath()).thenReturn("/path/to/current/page"); 117 | 118 | when(entryPage.getContentResource()).thenReturn(mock(Resource.class)); 119 | when(entryPage.getPath()).thenReturn("/path/to/entry/page"); 120 | 121 | // Requests 122 | SlingHttpServletRequest mockSlingHttpServletRequest = new MockSlingHttpServletRequest(MockOsgi.newBundleContext()); 123 | mockSlingHttpServletRequest.setAttribute(ATTR_HIERARCHY_ENTRY_POINT_PAGE, entryPage); 124 | 125 | request = spy(mockSlingHttpServletRequest); 126 | when(request.getRequestParameter(eq(PN_STRUCTURE_PATTERNS.toLowerCase()))).thenReturn(requestParameter); 127 | when(request.adaptTo(Page.class)).thenReturn(currentPage); 128 | } 129 | 130 | @Test 131 | void testCreateHierarchyServletRequest() { 132 | // Mock request 133 | ComponentContext componentContext = spy(ComponentContext.class); 134 | when(request.getAttribute(ATTR_COMPONENT_CONTEXT)).thenReturn(componentContext); 135 | when(request.getAttribute(ATTR_CURRENT_PAGE)).thenReturn(page); 136 | 137 | SlingHttpServletRequest wrappedRequest = HierarchyUtils.createHierarchyServletRequest(request, page, entryPage); 138 | verify(request, times(1)).setAttribute(eq(ATTR_CURRENT_PAGE), eq(page)); 139 | verify(request, times(1)).setAttribute(eq(ATTR_HIERARCHY_ENTRY_POINT_PAGE), eq(entryPage)); 140 | assertEquals(componentContext, wrappedRequest.getAttribute(ATTR_COMPONENT_CONTEXT)); 141 | } 142 | 143 | @Test 144 | void testGetEntryPointWhenNotSet() { 145 | // No entry point set 146 | when(request.getAttribute(ATTR_HIERARCHY_ENTRY_POINT_PAGE)).thenReturn(null); 147 | assertNull(HierarchyUtils.getEntryPoint(request)); 148 | } 149 | 150 | @Test 151 | void testGetEntryPointWhenSet() { 152 | // Entry point set 153 | when(request.getAttribute(ATTR_HIERARCHY_ENTRY_POINT_PAGE)).thenReturn(entryPage); 154 | assertEquals(entryPage, HierarchyUtils.getEntryPoint(request)); 155 | } 156 | 157 | @Test 158 | void testAddEntryPointPage() { 159 | // Mock pages 160 | List descendedPages = new ArrayList<>(); 161 | 162 | HierarchyUtils.addEntryPointPage(request, currentPage, descendedPages); 163 | assertEquals(entryPage, descendedPages.get(0)); 164 | } 165 | 166 | @Test 167 | void testGetStructurePatternsEmpty() { 168 | // Expect empty 169 | when(requestParameter.getString()).thenReturn(""); 170 | assertTrue(HierarchyUtils.getStructurePatterns(request, null).isEmpty()); 171 | } 172 | 173 | @Test 174 | void testGetStructurePatternsTwoElements() { 175 | // Expect two elements 176 | when(requestParameter.getString()).thenReturn("first,second"); 177 | List patterns = HierarchyUtils.getStructurePatterns(request, null); 178 | assertEquals("first", patterns.get(0).pattern()); 179 | assertEquals("second", patterns.get(1).pattern()); 180 | } 181 | 182 | @Nested 183 | @DisplayName("Tests for `getRootPage` function") 184 | class TestGetRootPage { 185 | @Mock 186 | ContentPolicyManager contentPolicyManager; 187 | 188 | @Mock 189 | ResourceResolver resourceResolver; 190 | 191 | HashMap pages = new HashMap<>(); 192 | 193 | HashMap templates = new HashMap<>(); 194 | 195 | HashMap pageContentResources = new HashMap<>(); 196 | 197 | HashMap pageContentPolicies = new HashMap<>(); 198 | 199 | HashMap properties = new HashMap<>(); 200 | 201 | @BeforeEach 202 | void beforeEach() { 203 | when(resource.getResourceResolver()).thenReturn(resourceResolver); 204 | when(resourceResolver.adaptTo(ContentPolicyManager.class)).thenReturn(contentPolicyManager); 205 | 206 | // Create mocks for three pages in a hierarchy 207 | for (String pageName : new String[] { "current", "parent", "root" }) { 208 | // Mock attributes 209 | pages.put(pageName, mock(Page.class)); 210 | templates.put(pageName, mock(Template.class)); 211 | pageContentResources.put(pageName, mock(Resource.class)); 212 | pageContentPolicies.put(pageName, mock(ContentPolicy.class)); 213 | properties.put(pageName, mock(ValueMap.class)); 214 | when(pages.get(pageName).getTemplate()).thenReturn(templates.get(pageName)); 215 | when(pages.get(pageName).getContentResource()).thenReturn(pageContentResources.get(pageName)); 216 | when(templates.get(pageName).hasStructureSupport()).thenReturn(true); 217 | when(contentPolicyManager.getPolicy(pageContentResources.get(pageName))).thenReturn(pageContentPolicies.get(pageName)); 218 | when(pageContentPolicies.get(pageName).getProperties()).thenReturn(properties.get(pageName)); 219 | when(properties.get(pageName).get(PN_IS_ROOT, false)).thenReturn(false); 220 | } 221 | 222 | // Mock hierarchical relationships 223 | when(pages.get("current").getParent()).thenReturn(pages.get("parent")); 224 | when(pages.get("parent").getParent()).thenReturn(pages.get("root")); 225 | } 226 | 227 | @Test 228 | void testGetRootPagePropertyMissingOrFalse() { 229 | // `PN_IS_ROOT` property is missing or set to `false` 230 | assertNull(HierarchyUtils.getRootPage(resource, pages.get("current"))); 231 | assertNull(HierarchyUtils.getRootPage(resource, pages.get("parent"))); 232 | assertNull(HierarchyUtils.getRootPage(resource, pages.get("root"))); 233 | } 234 | 235 | @Test 236 | void testGetRootPagePropertyTrue() { 237 | // `PN_IS_ROOT` set to `true` 238 | when(properties.get("root").get(PN_IS_ROOT, false)).thenReturn(true); 239 | assertEquals(pages.get("root"), HierarchyUtils.getRootPage(resource, pages.get("current"))); 240 | assertEquals(pages.get("root"), HierarchyUtils.getRootPage(resource, pages.get("parent"))); 241 | assertEquals(pages.get("root"), HierarchyUtils.getRootPage(resource, pages.get("root"))); 242 | } 243 | } 244 | 245 | @Test 246 | void testGetDescendantsNoPage() { 247 | // No page 248 | assertTrue(HierarchyUtils.getDescendants(null, request, structurePatterns, DEPTH).isEmpty()); 249 | } 250 | 251 | @Test 252 | void testGetDescendantsDepth0() { 253 | // Depth 0 254 | assertTrue(HierarchyUtils.getDescendants(page, request, structurePatterns, 0).isEmpty()); 255 | } 256 | 257 | @Test 258 | void testGetDescendantsIsChildPage() { 259 | // Is child page 260 | when(request.getAttribute(HierarchyConstants.ATTR_IS_CHILD_PAGE)).thenReturn(true); 261 | assertTrue(HierarchyUtils.getDescendants(page, request, structurePatterns, DEPTH).isEmpty()); 262 | when(request.getAttribute(HierarchyConstants.ATTR_IS_CHILD_PAGE)).thenReturn(false); 263 | } 264 | 265 | @Test 266 | void testGetDescendantsHasNoChildren() { 267 | // Has no children 268 | when(page.listChildren()).thenReturn(emptyIterator()); 269 | assertTrue(HierarchyUtils.getDescendants(page, request, structurePatterns, DEPTH).isEmpty()); 270 | } 271 | 272 | @Test 273 | void testGetDescendantsHasChildren() { 274 | // Has children 275 | ArrayList children = new ArrayList<>(); 276 | Page child1 = mock(Page.class); 277 | when(child1.getPath()).thenReturn("/path/to/child1"); 278 | children.add(child1); 279 | Page child2 = mock(Page.class); 280 | when(child2.getPath()).thenReturn("/path/to/child2"); 281 | children.add(child2); 282 | when(page.listChildren()).thenAnswer((Answer>) i -> children.listIterator()); 283 | assertEquals(children.size(), HierarchyUtils.getDescendants(page, request, structurePatterns, DEPTH).size()); 284 | 285 | // Has children and page filter (structurePatterns) 286 | structurePatterns.add(Pattern.compile("^/path/to/.*$")); 287 | assertEquals(2, HierarchyUtils.getDescendants(page, request, structurePatterns, DEPTH).size()); 288 | structurePatterns.set(0, Pattern.compile("child1")); 289 | assertEquals(1, HierarchyUtils.getDescendants(page, request, structurePatterns, DEPTH).size()); 290 | structurePatterns.set(0, Pattern.compile("thisdoesntmatchanychild")); 291 | assertEquals(0, HierarchyUtils.getDescendants(page, request, structurePatterns, DEPTH).size()); 292 | structurePatterns.clear(); 293 | 294 | // Has grandchildren 295 | ArrayList grandChildren = new ArrayList<>(); 296 | Page grandChild = mock(Page.class); 297 | when(grandChild.getPath()).thenReturn("/path/to/child2/grandchild"); 298 | grandChildren.add(grandChild); 299 | when(child2.listChildren()).thenAnswer((Answer>) i -> grandChildren.listIterator()); 300 | assertEquals(3, HierarchyUtils.getDescendants(page, request, structurePatterns, DEPTH).size()); 301 | 302 | // Has depth < actual children depth 303 | assertEquals(2, HierarchyUtils.getDescendants(page, request, structurePatterns, 1).size()); 304 | } 305 | 306 | @Test 307 | void testGetDescendantModelContentResourceIsNull() { 308 | // contentResource is null 309 | assertNull(HierarchyUtils.getDescendantModel(childPage, requestWrapper, modelFactory)); 310 | } 311 | 312 | @Test 313 | void testGetDescendantModelContentResourceIsNotNull() { 314 | // contentResource is not null 315 | when(childPage.getContentResource()).thenReturn(childPageContentResource); 316 | when(childPageContentResource.adaptTo(TemplatedResource.class)).thenReturn(templatedResource); 317 | when(modelFactory.getModelFromWrappedRequest(any(), any(), any())).thenReturn(childPageModel); 318 | assertEquals(childPageModel, HierarchyUtils.getDescendantModel(childPage, requestWrapper, modelFactory)); 319 | } 320 | 321 | @Nested 322 | @DisplayName("getDescendantsModels") 323 | class TestGetDescendantsModels { 324 | 325 | @Mock 326 | Style style; 327 | 328 | List children; 329 | 330 | @BeforeEach 331 | void beforeEach() { 332 | children = Arrays.asList( 333 | getMockPage("/path/to/child1"), 334 | getMockPage("/path/to/child2") 335 | ); 336 | 337 | when(currentPage.listChildren()).thenAnswer((Answer>) i -> children.listIterator()); 338 | 339 | when(modelFactory.getModelFromWrappedRequest(any(SlingHttpServletRequest.class), any(), any())) 340 | .thenAnswer(invocationOnMock -> new SpyPageImpl(invocationOnMock.getArgument(0))); 341 | 342 | when(style.get(PN_STRUCTURE_DEPTH, Integer.class)).thenReturn(2); 343 | } 344 | 345 | @Test 346 | void testPageWithNoChildren() { 347 | // having 348 | when(currentPage.listChildren()).thenReturn(null); 349 | 350 | // when 351 | final Map descendantsModels = 352 | HierarchyUtils.getDescendantsModels(request, currentPage, style, modelFactory); 353 | 354 | // then 355 | assertTrue(descendantsModels.containsKey(entryPage.getPath())); 356 | assertEquals(1, descendantsModels.size()); 357 | } 358 | 359 | @Test 360 | void testStyleWithZeroDepth() { 361 | // having 362 | when(style.get(PN_STRUCTURE_DEPTH, Integer.class)).thenReturn(0); 363 | 364 | // when 365 | final Map descendantsModels = 366 | HierarchyUtils.getDescendantsModels(request, currentPage, style, modelFactory); 367 | 368 | // then 369 | assertTrue(descendantsModels.containsKey(entryPage.getPath())); 370 | assertEquals(1, descendantsModels.size()); 371 | } 372 | 373 | @Test 374 | void testAllModels() { 375 | // when 376 | final Map descendantsModels = 377 | HierarchyUtils.getDescendantsModels(request, currentPage, style, modelFactory); 378 | 379 | // then 380 | assertTrue(descendantsModels.containsKey(entryPage.getPath())); 381 | assertEquals(children.size() + 1, descendantsModels.size()); 382 | children.forEach(child -> { 383 | final String childPath = child.getPath(); 384 | final SpyPageImpl page = (SpyPageImpl) descendantsModels.get(childPath); 385 | assertNotNull(page); 386 | 387 | assertComponentContext(page.getOriginalRequest(), childPath); 388 | }); 389 | } 390 | 391 | private void assertComponentContext(SlingHttpServletRequest request, String childPath) { 392 | final ComponentContext componentContext = WCMUtils.getComponentContext(request); 393 | assertEquals(childPath, componentContext.getPage().getPath(), "ComponentContext has wrong path"); 394 | } 395 | 396 | private Page getMockPage(String path) { 397 | final Resource resource = mock(Resource.class); 398 | when(resource.getPath()).thenReturn(path); 399 | 400 | final Page page = mock(Page.class); 401 | when(page.getPath()).thenReturn(path); 402 | when(page.getContentResource()).thenReturn(resource); 403 | 404 | return page; 405 | } 406 | 407 | private class SpyPageImpl implements com.adobe.aem.spa.project.core.models.Page { 408 | private final SlingHttpServletRequest originalRequest; 409 | 410 | public SpyPageImpl(SlingHttpServletRequest request) { 411 | super(); 412 | this.originalRequest = request; 413 | } 414 | 415 | SlingHttpServletRequest getOriginalRequest() { return originalRequest; } 416 | } 417 | 418 | } 419 | 420 | } 421 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.adobe.aem 5 | spa.project.core 6 | pom 7 | 1.3.19-SNAPSHOT 8 | AEM SPA Project Core 9 | spa-project-core 10 | https://github.com/adobe/aem-spa-project-core 11 | 12 | 13 | 14 | The Apache License, Version 2.0 15 | http://www.apache.org/licenses/LICENSE-2.0.txt 16 | 17 | 18 | 19 | 20 | all 21 | core 22 | ui.apps 23 | 24 | 25 | 26 | localhost 27 | 4502 28 | localhost 29 | 4503 30 | admin 31 | admin 32 | admin 33 | admin 34 | 2.26.0 35 | 4.2.0 36 | 2.5.3 37 | UTF-8 38 | UTF-8 39 | adobeinc 40 | https://sonarcloud.io 41 | adobe_aem-spa-project-core 42 | ${project.groupId}:${project.artifactId} 43 | 44 | 45 | 46 | scm:git:https://github.com/adobe/aem-spa-project-core.git 47 | scm:git:git@github.com:adobe/aem-spa-project-core.git 48 | https://github.com/adobe/aem-spa-project-core 49 | HEAD 50 | 51 | 52 | 53 | 54 | Grp-EditorXp 55 | editorxp@adobe.com 56 | Adobe 57 | http://opensource.adobe.com/ 58 | 59 | 60 | 61 | 62 | 63 | ossrh 64 | https://oss.sonatype.org/content/repositories/snapshots 65 | 66 | 67 | ossrh 68 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 69 | 70 | 71 | 72 | 73 | 74 | adobe-public-releases 75 | Adobe Public Repository 76 | https://repo.adobe.com/nexus/content/groups/public 77 | 78 | true 79 | never 80 | 81 | 82 | false 83 | 84 | 85 | 86 | 87 | 88 | 89 | adobe-public-releases 90 | Adobe Public Repository 91 | https://repo.adobe.com/nexus/content/groups/public 92 | 93 | true 94 | never 95 | 96 | 97 | false 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | org.apache.maven.plugins 107 | maven-release-plugin 108 | ${maven.release.version} 109 | 110 | 111 | org.apache.maven.release 112 | maven-release-oddeven-policy 113 | ${maven.release.version} 114 | 115 | 116 | org.apache.maven.scm 117 | maven-scm-provider-gitexe 118 | 1.9.4 119 | 120 | 121 | 122 | deploy 123 | true 124 | release 125 | false 126 | OddEvenVersionPolicy 127 | 128 | 129 | 130 | 131 | org.apache.maven.plugins 132 | maven-source-plugin 133 | 3.0.1 134 | 135 | 136 | attach-sources 137 | 138 | jar-no-fork 139 | 140 | 141 | 142 | 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-enforcer-plugin 147 | 148 | 149 | enforce-maven 150 | 151 | enforce 152 | 153 | 154 | 155 | 156 | [3.3.9,) 157 | 158 | 159 | Project must be compiled 160 | with Java 8 or higher 161 | 162 | 1.8.0 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | org.apache.maven.plugins 172 | maven-compiler-plugin 173 | 174 | 1.8 175 | 1.8 176 | 177 | 178 | 179 | 180 | 181 | 182 | com.github.eirslett 183 | frontend-maven-plugin 184 | 1.7.6 185 | 186 | v10.13.0 187 | 6.9.0 188 | 189 | 190 | 191 | install node and npm 192 | 193 | install-node-and-npm 194 | 195 | 196 | 197 | npm install 198 | 199 | npm 200 | 201 | 202 | 203 | 204 | 205 | 206 | org.apache.maven.plugins 207 | maven-jar-plugin 208 | 3.1.2 209 | 210 | 211 | 212 | org.apache.maven.plugins 213 | maven-clean-plugin 214 | 3.0.0 215 | 216 | 217 | 218 | biz.aQute.bnd 219 | bnd-maven-plugin 220 | ${bnd.version} 221 | 222 | 223 | biz.aQute.bnd 224 | bnd-baseline-maven-plugin 225 | ${bnd.version} 226 | 227 | 228 | 229 | org.apache.maven.plugins 230 | maven-resources-plugin 231 | 3.0.2 232 | 233 | 234 | 235 | org.apache.maven.plugins 236 | maven-compiler-plugin 237 | 3.6.1 238 | 239 | 240 | 241 | org.apache.maven.plugins 242 | maven-install-plugin 243 | 2.5.2 244 | 245 | 246 | 247 | org.apache.maven.plugins 248 | maven-surefire-plugin 249 | 2.22.1 250 | 251 | false 252 | 253 | 254 | 255 | 256 | org.apache.maven.plugins 257 | maven-failsafe-plugin 258 | 2.22.1 259 | 260 | 261 | 262 | org.apache.maven.plugins 263 | maven-deploy-plugin 264 | 2.8.2 265 | 266 | 267 | 268 | org.apache.sling 269 | sling-maven-plugin 270 | 2.4.0 271 | 272 | http://${aem.host}:${aem.port}/system/console 273 | WebConsole 274 | 275 | 276 | 277 | 278 | org.apache.sling 279 | htl-maven-plugin 280 | 1.0.6 281 | 282 | true 283 | 284 | 285 | 286 | 287 | validate 288 | 289 | 290 | 291 | 292 | 293 | 294 | org.apache.jackrabbit 295 | filevault-package-maven-plugin 296 | 1.0.3 297 | 298 | src/main/content/META-INF/vault/filter.xml 299 | 300 | 301 | 302 | 303 | com.day.jcr.vault 304 | content-package-maven-plugin 305 | 1.0.2 306 | 307 | http://${aem.host}:${aem.port}/crx/packmgr/service.jsp 308 | true 309 | ${vault.user} 310 | ${vault.password} 311 | 312 | 313 | 314 | 315 | org.apache.maven.plugins 316 | maven-enforcer-plugin 317 | 1.4.1 318 | 319 | 320 | 321 | org.apache.maven.plugins 322 | maven-dependency-plugin 323 | 3.0.0 324 | 325 | 326 | 327 | org.codehaus.mojo 328 | build-helper-maven-plugin 329 | 3.0.0 330 | 331 | 333 | 334 | org.eclipse.m2e 335 | lifecycle-mapping 336 | 1.0.0 337 | 338 | 339 | 340 | 341 | 342 | org.apache.maven.plugins 343 | maven-enforcer-plugin 344 | [1.0.0,) 345 | 346 | enforce 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | org.apache.maven.plugins 357 | 358 | 359 | maven-dependency-plugin 360 | 361 | 362 | [2.2,) 363 | 364 | 365 | copy-dependencies 366 | unpack 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | org.codehaus.mojo 377 | 378 | 379 | build-helper-maven-plugin 380 | 381 | 382 | [1.5,) 383 | 384 | 385 | 386 | reserve-network-port 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | adobe-public 408 | 409 | 410 | false 411 | 412 | 413 | 414 | ossrh 415 | Adobe Public Releases 416 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 417 | ossrh 418 | Adobe Public Snapshots 419 | https://oss.sonatype.org/content/repositories/snapshots 420 | 421 | 422 | 423 | 424 | 425 | autoInstallBundle 426 | 435 | 436 | false 437 | 438 | 439 | 440 | 441 | 442 | org.apache.sling 443 | sling-maven-plugin 444 | 445 | 446 | install-bundle 447 | 448 | install 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | autoInstallPackage 460 | 461 | false 462 | 463 | 464 | 465 | 466 | 467 | org.apache.jackrabbit 468 | filevault-package-maven-plugin 469 | 470 | 471 | create-package 472 | 473 | package 474 | 475 | 476 | 477 | 478 | 479 | com.day.jcr.vault 480 | content-package-maven-plugin 481 | 482 | 483 | install-package 484 | 485 | install 486 | 487 | 488 | http://${aem.host}:${aem.port}/crx/packmgr/service.jsp 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | autoInstallPackagePublish 500 | 501 | false 502 | 503 | 504 | 505 | 506 | 507 | org.apache.jackrabbit 508 | filevault-package-maven-plugin 509 | 510 | 511 | create-package 512 | 513 | package 514 | 515 | 516 | 517 | 518 | 519 | com.day.jcr.vault 520 | content-package-maven-plugin 521 | 522 | 523 | install-package-publish 524 | 525 | install 526 | 527 | 528 | http://${aem.publish.host}:${aem.publish.port}/crx/packmgr/service.jsp 529 | 530 | 531 | 532 | 533 | 534 | org.sonarsource.scanner.maven 535 | sonar-maven-plugin 536 | 3.6.0.1398 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | release 546 | 547 | false 548 | 549 | 550 | 551 | 552 | org.apache.maven.plugins 553 | maven-javadoc-plugin 554 | 3.2.0 555 | 556 | 557 | attach-javadocs 558 | 559 | jar 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | org.apache.maven.plugins 568 | maven-gpg-plugin 569 | 1.6 570 | 571 | 572 | sign-artifacts 573 | verify 574 | 575 | sign 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | publish 587 | 588 | false 589 | 590 | 591 | 592 | 593 | org.apache.sling 594 | maven-sling-plugin 595 | 596 | ${sling.url.publish} 597 | false 598 | true 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | org.jetbrains 614 | annotations 615 | 18.0.0 616 | 617 | 618 | javax.inject 619 | javax.inject 620 | 1 621 | 622 | 623 | 624 | 625 | org.osgi 626 | org.osgi.annotation.versioning 627 | 1.1.0 628 | provided 629 | 630 | 631 | org.osgi 632 | org.osgi.annotation.bundle 633 | 1.0.0 634 | provided 635 | 636 | 637 | org.osgi 638 | org.osgi.service.metatype.annotations 639 | 1.4.0 640 | provided 641 | 642 | 643 | org.osgi 644 | org.osgi.service.component.annotations 645 | 1.4.0 646 | provided 647 | 648 | 649 | org.osgi 650 | org.osgi.service.component 651 | 1.4.0 652 | provided 653 | 654 | 655 | org.osgi 656 | org.osgi.service.cm 657 | 1.6.0 658 | provided 659 | 660 | 661 | org.osgi 662 | org.osgi.service.event 663 | 1.3.1 664 | provided 665 | 666 | 667 | org.osgi 668 | org.osgi.service.log 669 | 1.4.0 670 | provided 671 | 672 | 673 | org.osgi 674 | org.osgi.resource 675 | 1.0.0 676 | provided 677 | 678 | 679 | org.osgi 680 | org.osgi.framework 681 | 1.9.0 682 | provided 683 | 684 | 685 | 686 | org.slf4j 687 | slf4j-api 688 | 1.7.21 689 | provided 690 | 691 | 692 | 693 | com.adobe.aem 694 | uber-jar 695 | 6.4.2 696 | apis 697 | provided 698 | 699 | 700 | com.adobe.cq 701 | core.wcm.components.all 702 | zip 703 | ${core.wcm.components.version} 704 | provided 705 | 706 | 707 | com.adobe.cq 708 | core.wcm.components.core 709 | ${core.wcm.components.version} 710 | provided 711 | 712 | 713 | 714 | org.apache.sling 715 | org.apache.sling.models.api 716 | 1.3.6 717 | provided 718 | 719 | 720 | 721 | javax.servlet 722 | javax.servlet-api 723 | 3.1.0 724 | provided 725 | 726 | 727 | javax.servlet.jsp 728 | jsp-api 729 | 2.1 730 | provided 731 | 732 | 733 | 734 | javax.jcr 735 | jcr 736 | 2.0 737 | provided 738 | 739 | 740 | 741 | com.day.cq.wcm 742 | cq-wcm-taglib 743 | 5.7.4 744 | provided 745 | 746 | 747 | 748 | 749 | org.junit 750 | junit-bom 751 | 5.4.1 752 | pom 753 | import 754 | 755 | 756 | org.slf4j 757 | slf4j-simple 758 | 1.7.25 759 | test 760 | 761 | 762 | org.mockito 763 | mockito-core 764 | 2.25.1 765 | test 766 | 767 | 768 | org.mockito 769 | mockito-junit-jupiter 770 | 2.25.1 771 | test 772 | 773 | 774 | junit-addons 775 | junit-addons 776 | 1.4 777 | test 778 | 779 | 780 | io.wcm 781 | io.wcm.testing.aem-mock.junit5 782 | 2.5.2 783 | test 784 | 785 | 786 | uk.org.lidalia 787 | slf4j-test 788 | 1.0.1 789 | test 790 | 791 | 792 | 793 | 794 | 795 | --------------------------------------------------------------------------------