diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/Dockerfile b/Java-base/sling-org-apache-sling-caconfig-impl/Dockerfile
new file mode 100644
index 000000000..e208c4890
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/Dockerfile
@@ -0,0 +1,28 @@
+FROM ubuntu:22.04
+
+RUN export DEBIAN_FRONTEND=noninteractive \
+ && apt-get update \
+ && apt-get install -y software-properties-common \
+ && add-apt-repository ppa:deadsnakes/ppa \
+ && apt-get update \
+ && apt-get install -y \
+ build-essential \
+ git \
+ vim \
+ jq \
+ && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/list/*
+
+RUN apt-get -y install sudo \
+ openjdk-8-jdk \
+ maven
+
+RUN bash -c "echo 2 | update-alternatives --config java"
+
+COPY src /workspace
+WORKDIR /workspace
+
+RUN mvn install -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100 -DskipTests=true -DskipITs=true -Dtest=None -DfailIfNoTests=false
+
+RUN mvn test -V -B -Denforcer.skip=true -Dcheckstyle.skip=true -Dcobertura.skip=true -Drat.skip=true -Dlicense.skip=true -Dfindbugs.skip=true -Dgpg.skip=true -Dskip.npm=true -Dskip.gulp=true -Dskip.bower=true -Drat.numUnapprovedLicenses=100
+
+ENV TZ=Asia/Seoul
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/CODE_OF_CONDUCT.md b/Java-base/sling-org-apache-sling-caconfig-impl/src/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..0fa18e593
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/CODE_OF_CONDUCT.md
@@ -0,0 +1,22 @@
+
+Apache Software Foundation Code of Conduct
+====
+
+Being an Apache project, Apache Sling adheres to the Apache Software Foundation's [Code of Conduct](https://www.apache.org/foundation/policies/conduct.html).
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/CONTRIBUTING.md b/Java-base/sling-org-apache-sling-caconfig-impl/src/CONTRIBUTING.md
new file mode 100644
index 000000000..ac82a1abe
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/CONTRIBUTING.md
@@ -0,0 +1,24 @@
+
+Contributing
+====
+
+Thanks for choosing to contribute!
+
+You will find all the necessary details about how you can do this at https://sling.apache.org/contributing.html.
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/Jenkinsfile b/Java-base/sling-org-apache-sling-caconfig-impl/src/Jenkinsfile
new file mode 100644
index 000000000..f5825190c
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/Jenkinsfile
@@ -0,0 +1,20 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+slingOsgiBundleBuild()
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/LICENSE b/Java-base/sling-org-apache-sling-caconfig-impl/src/LICENSE
new file mode 100644
index 000000000..d64569567
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/README.md b/Java-base/sling-org-apache-sling-caconfig-impl/src/README.md
new file mode 100644
index 000000000..c23a6762c
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/README.md
@@ -0,0 +1,9 @@
+[ ](https://sling.apache.org)
+
+ [![Build Status](https://builds.apache.org/buildStatus/icon?job=Sling/sling-org-apache-sling-caconfig-impl/master)](https://builds.apache.org/job/Sling/job/sling-org-apache-sling-caconfig-impl/job/master) [![Test Status](https://img.shields.io/jenkins/t/https/builds.apache.org/job/Sling/job/sling-org-apache-sling-caconfig-impl/job/master.svg)](https://builds.apache.org/job/Sling/job/sling-org-apache-sling-caconfig-impl/job/master/test_results_analyzer/) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.apache.sling/org.apache.sling.caconfig.impl/badge.svg)](https://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.apache.sling%22%20a%3A%22org.apache.sling.caconfig.impl%22) [![JavaDocs](https://www.javadoc.io/badge/org.apache.sling/org.apache.sling.caconfig.impl.svg)](https://www.javadoc.io/doc/org.apache.sling/org.apache.sling.caconfig.impl) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) [![caconfig](https://sling.apache.org/badges/group-caconfig.svg)](https://github.com/apache/sling-aggregator/blob/master/docs/groups/caconfig.md)
+
+# Apache Sling Context-Aware Configuration Implementation
+
+This module is part of the [Apache Sling](https://sling.apache.org) project.
+
+Documentation: [Apache Sling Context-Aware Configuration](https://sling.apache.org/documentation/bundles/context-aware-configuration/context-aware-configuration.html)
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/pom.xml b/Java-base/sling-org-apache-sling-caconfig-impl/src/pom.xml
new file mode 100644
index 000000000..e1c869156
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/pom.xml
@@ -0,0 +1,256 @@
+
+
+
+ 4.0.0
+
+
+ org.apache.sling
+ sling-bundle-parent
+ 36
+
+
+
+ org.apache.sling.caconfig.impl
+ jar
+ 1.5.1-SNAPSHOT
+ Apache Sling Context-Aware Configuration Implementation
+ Apache Sling Context-Aware Configuration Implementation
+
+
+ scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-caconfig-impl.git
+ scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-caconfig-impl.git
+ https://gitbox.apache.org/repos/asf?p=sling-org-apache-sling-caconfig-impl.git
+ HEAD
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+
+
+ bnd-process
+
+ bnd-process
+
+
+
+ Import-Package: \
+ \
+ org.apache.felix.webconsole;resolution:=optional,\
+ \
+ org.apache.sling.xss;version="[1.0.0,3)",\
+ \
+ !org.apache.sling.commons.osgi,\
+ *
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-baseline-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.0.0
+
+
+ package
+
+ shade
+
+
+ true
+ true
+
+
+ org.apache.sling:org.apache.sling.commons.osgi
+
+
+
+
+ org.apache.sling.commons.osgi
+ caconfigimpl.org.apache.sling.commons.osgi
+
+
+
+
+ org.apache.sling:org.apache.sling.commons.osgi
+
+ org/apache/sling/commons/osgi/**
+
+
+
+
+
+
+
+
+ org.apache.rat
+ apache-rat-plugin
+
+
+ dependency-reduced-pom.xml
+
+
+
+
+
+
+
+
+ org.osgi
+ org.osgi.annotation.versioning
+ provided
+
+
+
+ org.apache.sling
+ org.apache.sling.commons.osgi
+ 2.4.0
+ compile
+
+
+ org.apache.sling
+ org.apache.sling.caconfig.api
+ 1.2.0
+ compile
+
+
+ org.apache.sling
+ org.apache.sling.caconfig.spi
+ 1.3.2
+ compile
+
+
+ org.jetbrains
+ annotations
+ provided
+
+
+ org.slf4j
+ slf4j-api
+ provided
+
+
+ org.osgi
+ osgi.core
+ provided
+
+
+ org.apache.sling
+ org.apache.sling.api
+ 2.9.0
+ provided
+
+
+ org.apache.sling
+ org.apache.sling.scripting.api
+ 2.1.6
+ provided
+
+
+ org.apache.commons
+ commons-lang3
+ 3.3.2
+ compile
+
+
+ org.apache.commons
+ commons-collections4
+ 4.1
+ compile
+
+
+ org.apache.geronimo.specs
+ geronimo-json_1.0_spec
+ 1.0-alpha-1
+ provided
+
+
+
+
+ org.apache.felix
+ org.apache.felix.webconsole
+ 4.2.0
+ provided
+
+
+ org.apache.felix
+ org.apache.felix.inventory
+ 1.0.4
+ provided
+
+
+ org.apache.sling
+ org.apache.sling.xss
+ 1.0.0
+ provided
+
+
+
+
+ org.apache.sling
+ org.apache.sling.testing.osgi-mock
+ 2.2.4
+ test
+
+
+ org.apache.sling
+ org.apache.sling.testing.sling-mock
+ 1.9.6
+ test
+
+
+ org.apache.sling
+ org.apache.sling.testing.hamcrest
+ 1.0.2
+ test
+
+
+ junit
+ junit
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ test
+
+
+ org.mockito
+ mockito-core
+ 2.0.87-beta
+ test
+
+
+ org.apache.johnzon
+ johnzon-core
+ 1.0.0
+ test
+
+
+
+
+
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationBindingsValueProvider.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationBindingsValueProvider.java
new file mode 100644
index 000000000..37e87be98
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationBindingsValueProvider.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.script.Bindings;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.scripting.SlingBindings;
+import org.apache.sling.caconfig.ConfigurationBuilder;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationMetadataProviderMultiplexer;
+import org.apache.sling.caconfig.spi.ConfigurationMetadataProvider;
+import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata;
+import org.apache.sling.scripting.api.BindingsValuesProvider;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+/**
+ * Binds a script variable "caconfig" to the current configuration value map to be
+ * used in HTL (Sightly).
+ */
+@Component(immediate = true, service = BindingsValuesProvider.class, property = {
+ "javax.script.name=sightly",
+ Constants.SERVICE_RANKING + "=100" })
+@Designate(ocd = ConfigurationBindingsValueProvider.Config.class)
+public class ConfigurationBindingsValueProvider implements BindingsValuesProvider {
+
+ /**
+ * Name of the variable to which the config value map is bound to in script
+ * configuration.
+ */
+ public static final String BINDING_VARIABLE = "caconfig";
+
+ @ObjectClassDefinition(name = "Apache Sling Context-Aware Configuration HTL Binding Values Provider",
+ description = "Binds a script variable '" + BINDING_VARIABLE + "' to the HTL/Sightly scripting context.")
+ static @interface Config {
+
+ @AttributeDefinition(name = "Enabled", description = "Enable provider.")
+ boolean enabled() default true;
+
+ }
+
+ @Reference
+ private ConfigurationMetadataProviderMultiplexer configMetadataProvider;
+
+ private boolean enabled;
+
+ @Override
+ @SuppressWarnings("unused")
+ public void addBindings(Bindings bindings) {
+ if (!enabled || !bindings.containsKey(SlingBindings.REQUEST)) {
+ return;
+ }
+ SlingHttpServletRequest request = (SlingHttpServletRequest)bindings.get(SlingBindings.REQUEST);
+ Resource resource = request.getResource();
+ if (resource == null) {
+ return;
+ }
+ Map configMap = new ConfigMap(resource, configMetadataProvider);
+ bindings.put(BINDING_VARIABLE, configMap);
+ }
+
+ @Activate
+ void activate(Config config) {
+ this.enabled = config.enabled();
+ }
+
+
+ /**
+ * This is a "virtual" containing configuration names as keys, and the underlying value maps/value map collections as values.
+ * The map accesses only the data that is really required in a lazy fashion.
+ */
+ private static class ConfigMap implements Map {
+
+ private final Resource resource;
+ private final ConfigurationMetadataProvider configMetadataProvider;
+ private Set configNamesCache;
+ private Map valuesCache = new HashMap<>();
+
+ ConfigMap(Resource resource, ConfigurationMetadataProvider configMetadataProvider) {
+ this.resource = resource;
+ this.configMetadataProvider = configMetadataProvider;
+ }
+
+ private Set getConfigNames() {
+ if (configNamesCache == null) {
+ configNamesCache = configMetadataProvider.getConfigurationNames();
+ }
+ return configNamesCache;
+ }
+
+ @Override
+ public int size() {
+ return getConfigNames().size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return getConfigNames().isEmpty();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return getConfigNames().contains(key);
+ }
+
+ @Override
+ public Object get(Object key) {
+ Object value = valuesCache.get(key);
+ if (value == null) {
+ value = getConfigValue((String)key);
+ if (value != null) {
+ valuesCache.put((String)key, value);
+ }
+ }
+ return value;
+ }
+
+ private Object getConfigValue(String configName) {
+ @SuppressWarnings("null")
+ ConfigurationBuilder configBuilder = resource.adaptTo(ConfigurationBuilder.class).name(configName);
+ if (isCollection(configName)) {
+ return configBuilder.asValueMapCollection();
+ }
+ else {
+ return configBuilder.asValueMap();
+ }
+ }
+
+ private boolean isCollection(String configName) {
+ ConfigurationMetadata configMetadata = configMetadataProvider.getConfigurationMetadata(configName);
+ if (configMetadata != null) {
+ return configMetadata.isCollection();
+ }
+ else {
+ return false;
+ }
+ }
+
+ @Override
+ public Set keySet() {
+ return getConfigNames();
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object put(String key, Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object remove(Object key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void putAll(Map extends String, ? extends Object> m) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Collection values() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Set> entrySet() {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationBuilderAdapterFactory.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationBuilderAdapterFactory.java
new file mode 100644
index 000000000..464b574db
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationBuilderAdapterFactory.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl;
+
+import org.apache.sling.api.adapter.AdapterFactory;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.caconfig.ConfigurationBuilder;
+import org.apache.sling.caconfig.ConfigurationResolver;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+
+@Component(service=AdapterFactory.class,
+ property={
+ AdapterFactory.ADAPTER_CLASSES + "=org.apache.sling.caconfig.ConfigurationBuilder",
+ AdapterFactory.ADAPTABLE_CLASSES + "=org.apache.sling.api.resource.Resource"
+ })
+public class ConfigurationBuilderAdapterFactory implements AdapterFactory {
+
+ @Reference(policyOption = ReferencePolicyOption.GREEDY)
+ private ConfigurationResolver resolver;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public AdapterType getAdapter(@NotNull final Object adaptable, @NotNull final Class type) {
+ if (adaptable instanceof Resource && type == ConfigurationBuilder.class) {
+ return (AdapterType) resolver.get((Resource) adaptable);
+ }
+ return null;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationBuilderImpl.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationBuilderImpl.java
new file mode 100644
index 000000000..307de4e23
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationBuilderImpl.java
@@ -0,0 +1,429 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.collections4.IteratorUtils;
+import org.apache.commons.collections4.ResettableListIterator;
+import org.apache.commons.collections4.Transformer;
+import org.apache.commons.collections4.iterators.ListIteratorWrapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.caconfig.ConfigurationBuilder;
+import org.apache.sling.caconfig.ConfigurationResolveException;
+import org.apache.sling.caconfig.ConfigurationResolver;
+import org.apache.sling.caconfig.impl.ConfigurationProxy.ChildResolver;
+import org.apache.sling.caconfig.impl.metadata.AnnotationClassParser;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationOverrideMultiplexer;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationPersistenceStrategyMultiplexer;
+import org.apache.sling.caconfig.resource.impl.util.ConfigNameUtil;
+import org.apache.sling.caconfig.resource.impl.util.MapUtil;
+import org.apache.sling.caconfig.resource.spi.ConfigurationResourceResolvingStrategy;
+import org.apache.sling.caconfig.spi.ConfigurationInheritanceStrategy;
+import org.apache.sling.caconfig.spi.ConfigurationMetadataProvider;
+import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata;
+import org.apache.sling.caconfig.spi.metadata.PropertyMetadata;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class ConfigurationBuilderImpl implements ConfigurationBuilder {
+
+ private final Resource contentResource;
+ private final ConfigurationResolver configurationResolver;
+ private final ConfigurationResourceResolvingStrategy configurationResourceResolvingStrategy;
+ private final ConfigurationPersistenceStrategyMultiplexer configurationPersistenceStrategy;
+ private final ConfigurationInheritanceStrategy configurationInheritanceStrategy;
+ private final ConfigurationOverrideMultiplexer configurationOverrideMultiplexer;
+ private final ConfigurationMetadataProvider configurationMetadataProvider;
+ private final Collection configBucketNames;
+ private final String configName;
+
+ private static final Logger log = LoggerFactory.getLogger(ConfigurationBuilderImpl.class);
+
+ public ConfigurationBuilderImpl(final Resource resource,
+ final ConfigurationResolver configurationResolver,
+ final ConfigurationResourceResolvingStrategy configurationResourceResolvingStrategy,
+ final ConfigurationPersistenceStrategyMultiplexer configurationPersistenceStrategy,
+ final ConfigurationInheritanceStrategy configurationInheritanceStrategy,
+ final ConfigurationOverrideMultiplexer configurationOverrideMultiplexer,
+ final ConfigurationMetadataProvider configurationMetadataProvider,
+ final Collection configBucketNames) {
+ this(resource, configurationResolver, configurationResourceResolvingStrategy, configurationPersistenceStrategy,
+ configurationInheritanceStrategy, configurationOverrideMultiplexer, configurationMetadataProvider, configBucketNames, null);
+ }
+
+ private ConfigurationBuilderImpl(final Resource resource,
+ final ConfigurationResolver configurationResolver,
+ final ConfigurationResourceResolvingStrategy configurationResourceResolvingStrategy,
+ final ConfigurationPersistenceStrategyMultiplexer configurationPersistenceStrategy,
+ final ConfigurationInheritanceStrategy configurationInheritanceStrategy,
+ final ConfigurationOverrideMultiplexer configurationOverrideMultiplexer,
+ final ConfigurationMetadataProvider configurationMetadataProvider,
+ final Collection configBucketNames,
+ final String configName) {
+ this.contentResource = resource;
+ this.configurationResolver = configurationResolver;
+ this.configurationResourceResolvingStrategy = configurationResourceResolvingStrategy;
+ this.configurationPersistenceStrategy = configurationPersistenceStrategy;
+ this.configurationInheritanceStrategy = configurationInheritanceStrategy;
+ this.configurationOverrideMultiplexer = configurationOverrideMultiplexer;
+ this.configurationMetadataProvider = configurationMetadataProvider;
+ this.configBucketNames = configBucketNames;
+ this.configName = configName;
+ }
+
+ @Override
+ public @NotNull ConfigurationBuilder name(@NotNull final String configName) {
+ ConfigNameUtil.ensureValidConfigName(configName);
+ return new ConfigurationBuilderImpl(contentResource,
+ configurationResolver,
+ configurationResourceResolvingStrategy,
+ configurationPersistenceStrategy,
+ configurationInheritanceStrategy,
+ configurationOverrideMultiplexer,
+ configurationMetadataProvider,
+ configBucketNames,
+ configName);
+ }
+
+ /**
+ * Validate the configuration name.
+ * @param name Configuration name or relative path
+ */
+ private void validateConfigurationName(String name) {
+ if (name == null) {
+ throw new ConfigurationResolveException("Configuration name is required.");
+ }
+ }
+
+ /**
+ * Converts configuration resource into given class.
+ * @param Target class
+ */
+ private interface Converter {
+ T convert(Resource resource, Class clazz, String configName, boolean isCollection);
+ }
+
+ /**
+ * Get singleton configuration resource and convert it to the desired target class.
+ * @param configName Configuration name
+ * @param clazz Target class
+ * @param converter Conversion method
+ * @return Converted singleton configuration
+ */
+ private T getConfigResource(String configName, Class clazz, Converter converter) {
+ Iterator resourceInheritanceChain = null;
+ if (this.contentResource != null) {
+ validateConfigurationName(configName);
+ resourceInheritanceChain = this.configurationResourceResolvingStrategy
+ .getResourceInheritanceChain(this.contentResource, configBucketNames, configName);
+ }
+ return convert(resourceInheritanceChain, clazz, converter, configName, false);
+ }
+
+ /**
+ * Get configuration resource collection and convert it to the desired target class.
+ * @param configName Configuration name
+ * @param clazz Target class
+ * @param converter Conversion method
+ * @return Converted configuration collection
+ */
+ private Collection getConfigResourceCollection(String configName, Class clazz, Converter converter) {
+ if (this.contentResource != null) {
+ validateConfigurationName(configName);
+
+ // get all possible colection parent config names
+ Collection collectionParentConfigNames = configurationPersistenceStrategy.getAllCollectionParentConfigNames(configName);
+ List> resourceInheritanceChains = new ArrayList<>();
+ for (String collectionParentConfigName : collectionParentConfigNames) {
+ Collection> result = this.configurationResourceResolvingStrategy
+ .getResourceCollectionInheritanceChain(this.contentResource, configBucketNames, collectionParentConfigName);
+ if (result != null) {
+ resourceInheritanceChains.addAll(result);
+ }
+ }
+
+ final Collection result = new ArrayList<>();
+ for (final Iterator resourceInheritanceChain : resourceInheritanceChains) {
+ final T obj = convert(resourceInheritanceChain, clazz, converter, configName, true);
+ if (obj != null) {
+ result.add(obj);
+ }
+ }
+ return result;
+ }
+ else {
+ return Collections.emptyList();
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private T convert(final Iterator resourceInhertianceChain, final Class clazz, final Converter converter,
+ final String name, final boolean isCollection) {
+ Resource configResource = null;
+ String conversionName = name;
+ if (resourceInhertianceChain != null) {
+ ResettableListIterator resettableResourceInhertianceChain = new ListIteratorWrapper(resourceInhertianceChain);
+ // apply persistence transformation
+ Iterator transformedResources = IteratorUtils.transformedIterator(resettableResourceInhertianceChain,
+ new Transformer() {
+ @Override
+ public Object transform(Object input) {
+ if (isCollection) {
+ return configurationPersistenceStrategy.getCollectionItemResource((Resource)input);
+ }
+ else {
+ return configurationPersistenceStrategy.getResource((Resource)input);
+ }
+ }
+ });
+ // apply resource inheritance
+ configResource = configurationInheritanceStrategy.getResource(transformedResources);
+ // apply overrides
+ configResource = configurationOverrideMultiplexer.overrideProperties(contentResource.getPath(), name, configResource, contentResource.getResourceResolver());
+ // build name
+ if (isCollection) {
+ // get untransformed resource for getting collection item name
+ resettableResourceInhertianceChain.reset();
+ Resource untransformedConfigResource = configurationInheritanceStrategy.getResource(resettableResourceInhertianceChain);
+ if (untransformedConfigResource != null && configResource != null) {
+ conversionName = configurationPersistenceStrategy.getCollectionParentConfigName(conversionName, configResource.getPath())
+ + "/" + untransformedConfigResource.getName();
+ }
+ }
+ }
+ if (log.isTraceEnabled() && configResource != null) {
+ log.trace("+ Found config resource for context path " + contentResource.getPath() + ": " + configResource.getPath() + " "
+ + MapUtil.traceOutput(configResource.getValueMap()));
+ }
+
+ // if no config resource found still check for overrides
+ if (configResource == null && contentResource != null) {
+ configResource = configurationOverrideMultiplexer.overrideProperties(contentResource.getPath(), name, (Resource)null, contentResource.getResourceResolver());
+ }
+
+ return converter.convert(configResource, clazz, conversionName, isCollection);
+ }
+
+ /**
+ * Apply default values from configuration metadata (where no real data is present).
+ * @param resource Resource
+ * @param configName Configuration name
+ * @return null if no default values found, or a wrapped resource with added default properties.
+ */
+ private Resource applyDefaultValues(Resource resource, String configName) {
+ if (resource == null) {
+ return null;
+ }
+ Map updatedMap = applyDefaultValues(resource.getValueMap(), configName);
+ if (updatedMap == null) {
+ return resource;
+ }
+ return new ConfigurationResourceWrapper(resource, new ValueMapDecorator(updatedMap));
+ }
+
+ /**
+ * Apply default values from configuration metadata (where no real data is present).
+ * @param props Properties
+ * @param configName Configuration name
+ * @return null if no default values found, or a new map with added default properties.
+ */
+ private Map applyDefaultValues(Map props, String configName) {
+ ConfigurationMetadata metadata = configurationMetadataProvider.getConfigurationMetadata(configName);
+ if (metadata == null) {
+ // probably a configuration list - remove item name from end
+ if (StringUtils.contains(configName, "/")) {
+ String partialConfigName = StringUtils.substringBeforeLast(configName, "/");
+ metadata = configurationMetadataProvider.getConfigurationMetadata(partialConfigName);
+ }
+ if (metadata == null) {
+ return null;
+ }
+ }
+ Map updatedMap = new HashMap<>();
+ for (PropertyMetadata> propertyMetadata : metadata.getPropertyMetadata().values()) {
+ if (propertyMetadata.getDefaultValue() != null) {
+ updatedMap.put(propertyMetadata.getName(), propertyMetadata.getDefaultValue());
+ }
+ }
+ if (updatedMap.isEmpty()) {
+ return null;
+ }
+ updatedMap.putAll(props);
+ return updatedMap;
+ }
+
+ // --- Annotation class support ---
+
+ @Override
+ public @NotNull T as(@NotNull final Class clazz) {
+ final String name = getConfigurationNameForAnnotationClass(clazz);
+ if (log.isDebugEnabled()) {
+ log.debug("Get configuration for context path {}, name '{}', class {}", contentResource.getPath(), name, clazz.getName());
+ }
+ return getConfigResource(name, clazz, new AnnotationConverter());
+ }
+
+ @Override
+ public @NotNull Collection asCollection(@NotNull Class clazz) {
+ final String name = getConfigurationNameForAnnotationClass(clazz);
+ if (log.isDebugEnabled()) {
+ log.debug("Get configuration collection for context path {}, name '{}', class {}", contentResource.getPath(), name, clazz.getName());
+ }
+ return getConfigResourceCollection(name, clazz, new AnnotationConverter());
+ }
+
+ private String getConfigurationNameForAnnotationClass(Class> clazz) {
+ if (this.configName != null) {
+ return this.configName;
+ }
+ else {
+ // derive configuration name from annotation class if no name specified
+ return AnnotationClassParser.getConfigurationName(clazz);
+ }
+ }
+
+ private class AnnotationConverter implements Converter {
+ @Override
+ public T convert(final Resource resource, final Class clazz, final String configName, final boolean isCollection) {
+ return ConfigurationProxy.get(resource, clazz, new ChildResolver() {
+ private ConfigurationBuilder getConfiguration(String nestedConfigName) {
+ String childName;
+ String relatedConfigPath = resource != null ? resource.getPath() : null;
+ if (isCollection) {
+ childName = configurationPersistenceStrategy.getCollectionItemConfigName(configName, relatedConfigPath) + "/" + nestedConfigName;
+ }
+ else {
+ childName = configurationPersistenceStrategy.getConfigName(configName, relatedConfigPath) + "/" + nestedConfigName;
+ }
+ return configurationResolver.get(contentResource).name(childName);
+ }
+ @Override
+ public C getChild(String configName, Class clazz) {
+ return getConfiguration(configName).as(clazz);
+ }
+ @Override
+ public Collection getChildren(String configName, Class clazz) {
+ return getConfiguration(configName).asCollection(clazz);
+ }
+ });
+ }
+ }
+
+ // --- ValueMap support ---
+
+ @Override
+ public @NotNull ValueMap asValueMap() {
+ if (log.isDebugEnabled()) {
+ log.debug("Get ValueMap for context path {}, name '{}'", contentResource.getPath(), this.configName);
+ }
+ return getConfigResource(this.configName, ValueMap.class, new ValueMapConverter());
+ }
+
+ @Override
+ public @NotNull Collection asValueMapCollection() {
+ if (log.isDebugEnabled()) {
+ log.debug("Get ValueMap collection for context path {}, name '{}'", contentResource.getPath(), this.configName);
+ }
+ return getConfigResourceCollection(this.configName, ValueMap.class, new ValueMapConverter());
+ }
+
+ private class ValueMapConverter implements Converter {
+ @Override
+ public ValueMap convert(Resource resource, Class clazz, String configName, boolean isCollection) {
+ ValueMap props = ResourceUtil.getValueMap(resource);
+ Map updatedMap = applyDefaultValues(props, configName);
+ if (updatedMap != null) {
+ return new ValueMapDecorator(updatedMap);
+ }
+ else {
+ return props;
+ }
+ }
+ }
+
+ // --- Adaptable support ---
+
+ @Override
+ public T asAdaptable(@NotNull Class clazz) {
+ if (log.isDebugEnabled()) {
+ log.debug("Get adaptable for context path {}, name '{}', class {}", contentResource.getPath(), this.configName, clazz);
+ }
+ return getConfigResource(this.configName, clazz, new AdaptableConverter());
+ }
+
+ @Override
+ public @NotNull Collection asAdaptableCollection(@NotNull Class clazz) {
+ if (log.isDebugEnabled()) {
+ log.debug("Get adaptable collection for context path {}, name '{}', class {}", contentResource.getPath(), this.configName, clazz);
+ }
+ return getConfigResourceCollection(this.configName, clazz, new AdaptableConverter());
+ }
+
+ private class AdaptableConverter implements Converter {
+ @Override
+ public T convert(Resource resource, Class clazz, String configName, boolean isCollection) {
+ if (resource == null || clazz == ConfigurationBuilder.class) {
+ return null;
+ }
+ return applyDefaultValues(resource, configName).adaptTo(clazz);
+ }
+ }
+
+ // --- Config Node Existence Check Support ---
+
+ @Override
+ public boolean has(@NotNull Class clazz) {
+ final String name = getConfigurationNameForAnnotationClass(clazz);
+ if (log.isDebugEnabled()) {
+ log.debug("Check configuration for context path {}, name '{}', class {}", contentResource.getPath(), name, clazz.getName());
+ }
+ return checkIfConfigNodeExists(name);
+ }
+
+ @Override
+ public boolean has(@NotNull String configName) {
+ if (log.isDebugEnabled()) {
+ log.debug("Check configuration for context path {}, configuration name '{}' ", contentResource.getPath(), configName);
+ }
+ return checkIfConfigNodeExists(configName);
+ }
+
+ private boolean checkIfConfigNodeExists(String configName) {
+ Resource configResource = null;
+ if (this.contentResource != null) {
+ validateConfigurationName(configName);
+ configResource = this.configurationResourceResolvingStrategy
+ .getResource(this.contentResource, configBucketNames, configName);
+ }
+ return configResource != null ? true : false;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationInheritanceStrategyMultiplexerImpl.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationInheritanceStrategyMultiplexerImpl.java
new file mode 100644
index 000000000..2822bea90
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationInheritanceStrategyMultiplexerImpl.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.collections4.ResettableListIterator;
+import org.apache.commons.collections4.iterators.ListIteratorWrapper;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationInheritanceStrategyMultiplexer;
+import org.apache.sling.caconfig.spi.ConfigurationInheritanceStrategy;
+import org.apache.sling.commons.osgi.Order;
+import org.apache.sling.commons.osgi.RankedServices;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+
+/**
+ * Detects all {@link ConfigurationInheritanceStrategy} implementations in the container
+ * and consolidates their result based on service ranking.
+ */
+@Component(service = ConfigurationInheritanceStrategyMultiplexer.class,
+reference={
+ @Reference(name="configurationInheritanceStrategy", service=ConfigurationInheritanceStrategy.class,
+ bind="bindConfigurationInheritanceStrategy", unbind="unbindConfigurationInheritanceStrategy",
+ cardinality=ReferenceCardinality.MULTIPLE,
+ policy=ReferencePolicy.DYNAMIC, policyOption=ReferencePolicyOption.GREEDY)
+})
+public class ConfigurationInheritanceStrategyMultiplexerImpl implements ConfigurationInheritanceStrategyMultiplexer {
+
+ private RankedServices items = new RankedServices<>(Order.DESCENDING);
+
+ protected void bindConfigurationInheritanceStrategy(ConfigurationInheritanceStrategy item, Map props) {
+ items.bind(item, props);
+ }
+
+ protected void unbindConfigurationInheritanceStrategy(ConfigurationInheritanceStrategy item, Map props) {
+ items.unbind(item, props);
+ }
+
+ /**
+ * Get result from first strategy implementation that has an answer.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public Resource getResource(@NotNull Iterator configResources) {
+ List itemList = items.getList();
+ if (itemList.isEmpty()) {
+ return null;
+ }
+ else if (itemList.size() == 1) {
+ return itemList.get(0).getResource(configResources);
+ }
+ else {
+ ResettableListIterator resettableConfigResources = new ListIteratorWrapper(configResources);
+ for (ConfigurationInheritanceStrategy item : items) {
+ Resource result = item.getResource(resettableConfigResources);
+ if (result != null) {
+ return result;
+ }
+ else {
+ resettableConfigResources.reset();
+ }
+ }
+ return null;
+ }
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationNameConstants.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationNameConstants.java
new file mode 100644
index 000000000..d085310ee
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationNameConstants.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl;
+
+public final class ConfigurationNameConstants {
+
+ private ConfigurationNameConstants() {
+ // constants only
+ }
+
+ /**
+ * Bucket name for configuration data (configuration with key/value pairs).
+ */
+ public static final String CONFIGS_BUCKET_NAME = "sling:configs";
+
+ /**
+ * Bundle header defining list of class names with all configuration annotation classes in this bundle.
+ */
+ public static final String CONFIGURATION_CLASSES_HEADER = "Sling-ContextAware-Configuration-Classes";
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationPersistenceStrategyBridge.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationPersistenceStrategyBridge.java
new file mode 100644
index 000000000..f8600784d
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationPersistenceStrategyBridge.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl;
+
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.caconfig.spi.ConfigurationCollectionPersistData;
+import org.apache.sling.caconfig.spi.ConfigurationPersistData;
+import org.apache.sling.caconfig.spi.ConfigurationPersistenceStrategy;
+import org.apache.sling.caconfig.spi.ConfigurationPersistenceStrategy2;
+import org.apache.sling.commons.osgi.Order;
+import org.apache.sling.commons.osgi.ServiceUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+
+/**
+ * Bridges services implementing the deprecated {@link ConfigurationPersistenceStrategy} interface
+ * to the {@link ConfigurationPersistenceStrategy2} interface for backwards compatibility.
+ */
+@Component(reference={
+ @Reference(name="configurationPersistenceStrategy", service=ConfigurationPersistenceStrategy.class,
+ bind="bindConfigurationPersistenceStrategy", unbind="unbindConfigurationPersistenceStrategy",
+ cardinality=ReferenceCardinality.MULTIPLE,
+ policy=ReferencePolicy.DYNAMIC, policyOption=ReferencePolicyOption.GREEDY)
+})
+@SuppressWarnings("deprecation")
+public final class ConfigurationPersistenceStrategyBridge {
+
+ private volatile BundleContext bundleContext;
+ private final ConcurrentMap, ServiceRegistration> services = new ConcurrentHashMap<>();
+ private final ConcurrentMap, ServiceInfo> preActivateServices = new ConcurrentHashMap<>();
+
+ protected void bindConfigurationPersistenceStrategy(ConfigurationPersistenceStrategy item, Map props) {
+ ServiceInfo serviceInfo = new ServiceInfo(item, props);
+ Comparable key = ServiceUtil.getComparableForServiceRanking(props, Order.ASCENDING);
+ if (bundleContext != null) {
+ services.put(key, registerBridgeService(serviceInfo));
+ }
+ else {
+ preActivateServices.put(key, serviceInfo);
+ }
+ }
+
+ protected void unbindConfigurationPersistenceStrategy(ConfigurationPersistenceStrategy item, Map props) {
+ Comparable key = ServiceUtil.getComparableForServiceRanking(props, Order.ASCENDING);
+ unregisterBridgeService(services.remove(key));
+ }
+
+ /**
+ * Register {@link ConfigurationPersistenceStrategy2} bridge service for {@link ConfigurationPersistenceStrategy} service.
+ * @param serviceInfo Service information
+ * @return Service registration
+ */
+ private ServiceRegistration registerBridgeService(ServiceInfo serviceInfo) {
+ return bundleContext.registerService(ConfigurationPersistenceStrategy2.class,
+ new Adapter(serviceInfo.getService()), new Hashtable<>(serviceInfo.getProps()));
+ }
+
+ /**
+ * Unregister {@link ConfigurationPersistenceStrategy2} bridge service.
+ * @param service Service registration
+ */
+ private void unregisterBridgeService(ServiceRegistration service) {
+ if (service != null) {
+ service.unregister();
+ }
+ }
+
+ @Activate
+ private void activate(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ for (Map.Entry, ServiceInfo> entry : preActivateServices.entrySet()) {
+ services.put(entry.getKey(), registerBridgeService(entry.getValue()));
+ }
+ }
+
+
+ private static class ServiceInfo {
+ private final ConfigurationPersistenceStrategy service;
+ private final Map props;
+
+ public ServiceInfo(ConfigurationPersistenceStrategy service, Map props) {
+ this.service = service;
+ this.props = props;
+ }
+ public ConfigurationPersistenceStrategy getService() {
+ return service;
+ }
+ public Map getProps() {
+ return props;
+ }
+ }
+
+
+ /**
+ * Adapter which delegates {@link ConfigurationPersistenceStrategy2} methods to a {@link ConfigurationPersistenceStrategy} service.
+ */
+ public static class Adapter implements ConfigurationPersistenceStrategy2 {
+ private final ConfigurationPersistenceStrategy delegate;
+
+ public Adapter(ConfigurationPersistenceStrategy delegate) {
+ this.delegate = delegate;
+ }
+
+ /**
+ * @return Implementation class of the original service.
+ */
+ public Class> getOriginalServiceClass() {
+ return delegate.getClass();
+ }
+
+ @Override
+ public Resource getResource(@NotNull Resource resource) {
+ return delegate.getResource(resource);
+ }
+
+ @Override
+ public Resource getCollectionParentResource(@NotNull Resource resource) {
+ // with SPI/Impl 1.2 it was not possible to manipulate collection parent resource
+ return resource;
+ }
+
+ @Override
+ public Resource getCollectionItemResource(@NotNull Resource resource) {
+ return delegate.getResource(resource);
+ }
+
+ @Override
+ public String getResourcePath(@NotNull String resourcePath) {
+ return delegate.getResourcePath(resourcePath);
+ }
+
+ @Override
+ public String getCollectionParentResourcePath(@NotNull String resourcePath) {
+ // with SPI/Impl 1.2 it was not possible to manipulate collection parent resource
+ return resourcePath;
+ }
+
+ @Override
+ public String getCollectionItemResourcePath(@NotNull String resourcePath) {
+ return delegate.getResourcePath(resourcePath);
+ }
+
+ @Override
+ public String getConfigName(@NotNull String configName, @Nullable String relatedConfigPath) {
+ return delegate.getResourcePath(configName);
+ }
+
+ @Override
+ public String getCollectionParentConfigName(@NotNull String configName, @Nullable String relatedConfigPath) {
+ // with SPI/Impl 1.2 it was not possible to manipulate collection parent resource
+ return configName;
+ }
+
+ @Override
+ public String getCollectionItemConfigName(@NotNull String configName, @Nullable String relatedConfigPath) {
+ return delegate.getResourcePath(configName);
+ }
+
+ @Override
+ public boolean persistConfiguration(@NotNull ResourceResolver resourceResolver, @NotNull String configResourcePath,
+ @NotNull ConfigurationPersistData data) {
+ return delegate.persistConfiguration(resourceResolver, configResourcePath, data);
+ }
+
+ @Override
+ public boolean persistConfigurationCollection(@NotNull ResourceResolver resourceResolver,
+ @NotNull String configResourceCollectionParentPath, @NotNull ConfigurationCollectionPersistData data) {
+ return delegate.persistConfigurationCollection(resourceResolver, configResourceCollectionParentPath, data);
+ }
+
+ @Override
+ public boolean deleteConfiguration(@NotNull ResourceResolver resourceResolver, @NotNull String configResourcePath) {
+ return delegate.deleteConfiguration(resourceResolver, configResourcePath);
+ }
+
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationProxy.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationProxy.java
new file mode 100644
index 000000000..7a9cf9361
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationProxy.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.caconfig.ConfigurationResolveException;
+import org.apache.sling.caconfig.impl.metadata.AnnotationClassParser;
+import org.apache.sling.caconfig.spi.metadata.PropertyMetadata;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Maps the property of a resource to a dynamic proxy object implementing
+ * the annotation class defining the configuration parameters.
+ * Nested configurations with annotation classes referencing other annotation classes are also supported.
+ */
+final class ConfigurationProxy {
+
+ private ConfigurationProxy() {
+ // static methods only
+ }
+
+ /**
+ * Get dynamic proxy for given resources's properties mapped to given annotation class.
+ * @param resource Resource
+ * @param clazz Annotation class
+ * @param childResolver This is used to resolve nested configuration objects relative to the current configuration resource
+ * @return Dynamic proxy object
+ */
+ @SuppressWarnings("unchecked")
+ public @NotNull static T get(@Nullable Resource resource, @NotNull Class clazz, ChildResolver childResolver) {
+
+ // only annotation interface classes are supported
+ if (!clazz.isAnnotation()) {
+ throw new ConfigurationResolveException("Annotation interface class expected: " + clazz.getName());
+ }
+
+ // create dynamic proxy for annotation class accessing underlying resource properties
+ // wrap in caching invocation handler so client code can call all methods multiple times
+ // without having to worry about performance
+ return (T)Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz },
+ new CachingInvocationHandler(new DynamicProxyInvocationHandler(resource, childResolver)));
+ }
+
+ /**
+ * Resolves nested configurations.
+ */
+ public static interface ChildResolver {
+ T getChild(String configName, Class clazz);
+ Collection getChildren(String configName, Class clazz);
+ }
+
+ /**
+ * Maps resource properties to annotation class proxy, and support nested configurations.
+ */
+ static class DynamicProxyInvocationHandler implements InvocationHandler {
+
+ private final Resource resource;
+ private final ChildResolver childResolver;
+
+ private DynamicProxyInvocationHandler(Resource resource, ChildResolver childResolver) {
+ this.resource = resource;
+ this.childResolver = childResolver;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) {
+ String propName = AnnotationClassParser.getPropertyName(method.getName());
+
+ // check for nested configuration classes
+ Class> targetType = method.getReturnType();
+ Class> componentType = method.getReturnType();
+ boolean isArray = targetType.isArray();
+ if (isArray) {
+ componentType = targetType.getComponentType();
+ }
+ if (componentType.isAnnotation()) {
+ if (isArray) {
+ Collection> listItems = childResolver.getChildren(propName, componentType);
+ return listItems.toArray((Object[])Array.newInstance(componentType, listItems.size()));
+ }
+ else {
+ return childResolver.getChild(propName, componentType);
+ }
+ }
+
+ // validate type
+ if (!isValidType(componentType)) {
+ throw new ConfigurationResolveException("Unsupported type " + componentType.getName()
+ + " in " + method.getDeclaringClass() + "#" + method.getName());
+ }
+
+ // detect default value
+ Object defaultValue = method.getDefaultValue();
+ if (defaultValue == null) {
+ if (isArray) {
+ defaultValue = Array.newInstance(componentType, 0);
+ }
+ else if (targetType.isPrimitive()) {
+ // get default value for primitive data type (use hack via array)
+ defaultValue = Array.get(Array.newInstance(targetType, 1), 0);
+ }
+ }
+
+ // get value from valuemap with given type/default value
+ ValueMap props = ResourceUtil.getValueMap(resource);
+ Object value;
+ if (defaultValue != null) {
+ value = props.get(propName, defaultValue);
+ }
+ else {
+ value = props.get(propName, targetType);
+ }
+ return value;
+
+ }
+
+ /**
+ * Ensures the given type is support for reading configuration parameters.
+ * @param type Type
+ * @return true if type is supported
+ */
+ private boolean isValidType(Class> type) {
+ return PropertyMetadata.SUPPORTED_TYPES.contains(type);
+ }
+
+ }
+
+ /**
+ * Invocation handler that caches all results for each method name, and returns
+ * the result from cache on next invocation.
+ */
+ static class CachingInvocationHandler implements InvocationHandler {
+
+ private final InvocationHandler delegate;
+ private final Map results = new HashMap<>();
+ private static final Object NULL_OBJECT = new Object();
+
+ public CachingInvocationHandler(InvocationHandler delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ String key = method.getName();
+ Object result = results.get(key);
+ if (result == null) {
+ result = delegate.invoke(proxy, method, args);
+ if (result == null) {
+ result = NULL_OBJECT;
+ }
+ results.put(key, result);
+ }
+ if (result == NULL_OBJECT) {
+ return null;
+ }
+ else {
+ return result;
+ }
+ }
+
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationResolverImpl.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationResolverImpl.java
new file mode 100644
index 000000000..7a8212a88
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationResolverImpl.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl;
+
+import static org.apache.sling.caconfig.impl.ConfigurationNameConstants.CONFIGS_BUCKET_NAME;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.caconfig.ConfigurationBuilder;
+import org.apache.sling.caconfig.ConfigurationResolver;
+import org.apache.sling.caconfig.management.ConfigurationResourceResolverConfig;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationInheritanceStrategyMultiplexer;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationMetadataProviderMultiplexer;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationOverrideMultiplexer;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationPersistenceStrategyMultiplexer;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationResourceResolvingStrategyMultiplexer;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+@Component(service={ ConfigurationResolver.class, ConfigurationResourceResolverConfig.class }, immediate=true)
+@Designate(ocd=ConfigurationResolverImpl.Config.class)
+public class ConfigurationResolverImpl implements ConfigurationResolver, ConfigurationResourceResolverConfig {
+
+ @Reference
+ private ConfigurationResourceResolvingStrategyMultiplexer configurationResourceResolvingStrategy;
+ @Reference
+ private ConfigurationPersistenceStrategyMultiplexer configurationPersistenceStrategy;
+ @Reference
+ private ConfigurationInheritanceStrategyMultiplexer configurationInheritanceStrategy;
+ @Reference
+ private ConfigurationOverrideMultiplexer configurationOverrideMultiplexer;
+ @Reference
+ private ConfigurationMetadataProviderMultiplexer configurationMetadataProvider;
+
+ @ObjectClassDefinition(name="Apache Sling Context-Aware Configuration Resolver",
+ description="Getting context-aware configurations for a given resource context.")
+ static @interface Config {
+
+ @AttributeDefinition(name = "Config bucket names",
+ description = "Additional bucket resource names to '" + CONFIGS_BUCKET_NAME + "' to store configuration resources. "
+ + "The names are used in the order defined, always starting with " + CONFIGS_BUCKET_NAME + ". "
+ + "Once a bucket resource with a matching name is found, that bucket is used and the following names are skipped. "
+ + "For writeback via ConfigurationManager always " + CONFIGS_BUCKET_NAME + " is used.")
+ String[] configBucketNames();
+
+ }
+
+ private Collection configBucketNames;
+
+ @Activate
+ private void activate(Config config) {
+ configBucketNames = new ArrayList<>();
+ configBucketNames.add(ConfigurationNameConstants.CONFIGS_BUCKET_NAME);
+ if (!ArrayUtils.isEmpty(config.configBucketNames())) {
+ configBucketNames.addAll(Arrays.asList(config.configBucketNames()));
+ }
+ }
+
+ @Override
+ public @NotNull ConfigurationBuilder get(@NotNull Resource resource) {
+ return new ConfigurationBuilderImpl(resource, this,
+ configurationResourceResolvingStrategy, configurationPersistenceStrategy,
+ configurationInheritanceStrategy, configurationOverrideMultiplexer, configurationMetadataProvider,
+ configBucketNames);
+ }
+
+ @Override
+ public @NotNull Collection configBucketNames() {
+ return configBucketNames;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationResourceWrapper.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationResourceWrapper.java
new file mode 100644
index 000000000..938886c77
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/ConfigurationResourceWrapper.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl;
+
+import java.util.Iterator;
+
+import org.apache.sling.api.resource.AbstractResource;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceMetadata;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ValueMap;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Wrapper that returns an enhanced value map for the resource
+ * providing a merged map with all inherited property values.
+ * Unfortunately it's not possible to use {@link org.apache.sling.api.resource.ResourceWrapper} because when adapting
+ * to a Sling Model the replace valuemap would not take effect but the original value map.
+ */
+public final class ConfigurationResourceWrapper extends AbstractResource {
+
+ private final Resource resource;
+ private final ValueMap props;
+
+ public ConfigurationResourceWrapper(Resource resource, ValueMap props) {
+ this.resource = unwrap(resource);
+ this.props = props;
+ }
+
+ private static Resource unwrap(Resource resource) {
+ if (resource instanceof ConfigurationResourceWrapper) {
+ return ((ConfigurationResourceWrapper)resource).resource;
+ }
+ else {
+ return resource;
+ }
+ }
+
+ @SuppressWarnings({ "unchecked", "null" })
+ public AdapterType adaptTo(@NotNull Class type) {
+ if (type == ValueMap.class) {
+ return (AdapterType)props;
+ }
+ return super.adaptTo(type);
+ }
+
+ public ValueMap getValueMap() {
+ return props;
+ }
+
+ public @NotNull String getPath() {
+ return resource.getPath();
+ }
+
+ public String getName() {
+ return resource.getName();
+ }
+
+ public Resource getParent() {
+ return resource.getParent();
+ }
+
+ public Iterator listChildren() {
+ return resource.listChildren();
+ }
+
+ public Iterable getChildren() {
+ return resource.getChildren();
+ }
+
+ public Resource getChild(String relPath) {
+ return resource.getChild(relPath);
+ }
+
+ public @NotNull String getResourceType() {
+ return resource.getResourceType();
+ }
+
+ public String getResourceSuperType() {
+ return resource.getResourceSuperType();
+ }
+
+ public boolean hasChildren() {
+ return resource.hasChildren();
+ }
+
+ public boolean isResourceType(String resourceType) {
+ return resource.isResourceType(resourceType);
+ }
+
+ public @NotNull ResourceMetadata getResourceMetadata() {
+ return resource.getResourceMetadata();
+ }
+
+ public @NotNull ResourceResolver getResourceResolver() {
+ return resource.getResourceResolver();
+ }
+
+ /**
+ * @return Returns a string representation of this wrapper consisting of the class'
+ * simple name, the {@link #getResourceType() resource type} and
+ * {@link #getPath() path} as well as the string representation of the wrapped resource}.
+ */
+ @Override
+ public String toString() {
+ final String className;
+ if (getClass().getSimpleName().length() == 0) {
+ className = getClass().getName();
+ } else {
+ className = getClass().getSimpleName();
+ }
+ return className + ", type=" + getResourceType()
+ + ", path=" + getPath() + ", resource=[" + resource + "]";
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/def/ConfigurationDefNameConstants.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/def/ConfigurationDefNameConstants.java
new file mode 100644
index 000000000..ab38524d1
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/def/ConfigurationDefNameConstants.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.def;
+
+public final class ConfigurationDefNameConstants {
+
+ private ConfigurationDefNameConstants() {
+ // constants only
+ }
+
+ /**
+ * Boolean property that controls whether the properties (key/value pairs) of configuration resources
+ * should be inherited from the configuration hierarchy and merged.
+ */
+ public static final String PROPERTY_CONFIG_PROPERTY_INHERIT = "sling:configPropertyInherit";
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/def/DefaultConfigurationInheritanceStrategy.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/def/DefaultConfigurationInheritanceStrategy.java
new file mode 100644
index 000000000..1845075d3
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/def/DefaultConfigurationInheritanceStrategy.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.def;
+
+import static org.apache.sling.caconfig.impl.def.ConfigurationDefNameConstants.PROPERTY_CONFIG_PROPERTY_INHERIT;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.caconfig.impl.ConfigurationResourceWrapper;
+import org.apache.sling.caconfig.resource.impl.util.PropertyUtil;
+import org.apache.sling.caconfig.spi.ConfigurationInheritanceStrategy;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(service=ConfigurationInheritanceStrategy.class)
+@Designate(ocd=DefaultConfigurationInheritanceStrategy.Config.class)
+public class DefaultConfigurationInheritanceStrategy implements ConfigurationInheritanceStrategy {
+
+ @ObjectClassDefinition(name="Apache Sling Context-Aware Configuration Default Inheritance Strategy",
+ description="Standardized resource inheritance for configurations.")
+ public static @interface Config {
+
+ @AttributeDefinition(name="Enabled",
+ description = "Enable this configuration inheritance strategy.")
+ boolean enabled() default true;
+
+ @AttributeDefinition(name="Config property inheritance property names",
+ description = "Additional property names to " + PROPERTY_CONFIG_PROPERTY_INHERIT + " to handle property inheritance. The names are used in the order defined, "
+ + "always starting with " + PROPERTY_CONFIG_PROPERTY_INHERIT + ". Once a property with a value is found, that value is used and the following property names are skipped.")
+ String[] configPropertyInheritancePropertyNames();
+
+ }
+
+ private Config config;
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultConfigurationInheritanceStrategy.class);
+
+ @Activate
+ private void activate(final Config config) {
+ this.config = config;
+ }
+
+ @Override
+ public Resource getResource(@NotNull Iterator configResources) {
+ if (!config.enabled()) {
+ return null;
+ }
+ if (!configResources.hasNext()) {
+ return null;
+ }
+ Resource primary = configResources.next();
+ if (!isPropertyInheritance(primary) || !configResources.hasNext()) {
+ return primary;
+ }
+ Map mergedProps = getInheritedProperties(primary.getValueMap(), configResources);
+ return new ConfigurationResourceWrapper(primary, new ValueMapDecorator(mergedProps));
+ }
+
+ private boolean isPropertyInheritance(Resource resource) {
+ return PropertyUtil.getBooleanValueAdditionalKeys(resource.getValueMap(), PROPERTY_CONFIG_PROPERTY_INHERIT,
+ config.configPropertyInheritancePropertyNames());
+ }
+
+ private Map getInheritedProperties(Map parentProps, Iterator inheritanceChain) {
+ if (!inheritanceChain.hasNext()) {
+ return parentProps;
+ }
+ Resource next = inheritanceChain.next();
+ log.trace("! Property inheritance: Merge with properties from {}", next.getPath());
+ Map merged = new HashMap<>(next.getValueMap());
+ merged.putAll(parentProps);
+ if (isPropertyInheritance(next)) {
+ return getInheritedProperties(merged, inheritanceChain);
+ }
+ else {
+ return merged;
+ }
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/def/DefaultConfigurationPersistenceStrategy.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/def/DefaultConfigurationPersistenceStrategy.java
new file mode 100644
index 000000000..cf5571a81
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/def/DefaultConfigurationPersistenceStrategy.java
@@ -0,0 +1,276 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.def;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.caconfig.management.ConfigurationManagementSettings;
+import org.apache.sling.caconfig.management.impl.PropertiesFilterUtil;
+import org.apache.sling.caconfig.resource.impl.util.MapUtil;
+import org.apache.sling.caconfig.spi.ConfigurationCollectionPersistData;
+import org.apache.sling.caconfig.spi.ConfigurationPersistData;
+import org.apache.sling.caconfig.spi.ConfigurationPersistenceAccessDeniedException;
+import org.apache.sling.caconfig.spi.ConfigurationPersistenceException;
+import org.apache.sling.caconfig.spi.ConfigurationPersistenceStrategy2;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The default persistence strategy is quite simple: directly use the configuration resources.
+ * All existing properties are removed when new properties are stored in a singleton config resource.
+ * All existing child resources are removed when a new configs are stored for collection config resources.
+ */
+@Component(service = ConfigurationPersistenceStrategy2.class)
+@Designate(ocd=DefaultConfigurationPersistenceStrategy.Config.class)
+public class DefaultConfigurationPersistenceStrategy implements ConfigurationPersistenceStrategy2 {
+
+ @ObjectClassDefinition(name="Apache Sling Context-Aware Configuration Default Resource Persistence Strategy",
+ description="Directly uses configuration resources for storing configuration data.")
+ static @interface Config {
+
+ @AttributeDefinition(name="Enabled",
+ description = "Enable this configuration resource persistence strategy.")
+ boolean enabled() default true;
+
+ }
+
+ @Reference
+ private ConfigurationManagementSettings configurationManagementSettings;
+
+ private volatile Config config;
+
+ private static final Logger log = LoggerFactory.getLogger(DefaultConfigurationPersistenceStrategy.class);
+
+ @Activate
+ private void activate(ComponentContext componentContext, Config config) {
+ this.config = config;
+ }
+
+ @Override
+ public Resource getResource(@NotNull Resource resource) {
+ if (!config.enabled()) {
+ return null;
+ }
+ return resource;
+ }
+
+ @Override
+ public Resource getCollectionParentResource(@NotNull Resource resource) {
+ if (!config.enabled()) {
+ return null;
+ }
+ return resource;
+ }
+
+ @Override
+ public Resource getCollectionItemResource(@NotNull Resource resource) {
+ if (!config.enabled()) {
+ return null;
+ }
+ return resource;
+ }
+
+ @Override
+ public String getResourcePath(@NotNull String resourcePath) {
+ if (!config.enabled()) {
+ return null;
+ }
+ return resourcePath;
+ }
+
+ @Override
+ public String getCollectionParentResourcePath(@NotNull String resourcePath) {
+ if (!config.enabled()) {
+ return null;
+ }
+ return resourcePath;
+ }
+
+ @Override
+ public String getCollectionItemResourcePath(@NotNull String resourcePath) {
+ if (!config.enabled()) {
+ return null;
+ }
+ return resourcePath;
+ }
+
+ @Override
+ public String getConfigName(@NotNull String configName, @Nullable String relatedConfigPath) {
+ if (!config.enabled()) {
+ return null;
+ }
+ return configName;
+ }
+
+ @Override
+ public String getCollectionParentConfigName(@NotNull String configName, @Nullable String relatedConfigPath) {
+ if (!config.enabled()) {
+ return null;
+ }
+ return configName;
+ }
+
+ @Override
+ public String getCollectionItemConfigName(@NotNull String configName, @Nullable String relatedConfigPath) {
+ if (!config.enabled()) {
+ return null;
+ }
+ return configName;
+ }
+
+ @Override
+ public boolean persistConfiguration(@NotNull ResourceResolver resourceResolver, @NotNull String configResourcePath,
+ @NotNull ConfigurationPersistData data) {
+ if (!config.enabled()) {
+ return false;
+ }
+ getOrCreateResource(resourceResolver, configResourcePath, data.getProperties());
+ commit(resourceResolver, configResourcePath);
+ return true;
+ }
+
+ @Override
+ public boolean persistConfigurationCollection(@NotNull ResourceResolver resourceResolver, @NotNull String configResourceCollectionParentPath,
+ @NotNull ConfigurationCollectionPersistData data) {
+ if (!config.enabled()) {
+ return false;
+ }
+ Resource configResourceParent = getOrCreateResource(resourceResolver, configResourceCollectionParentPath, data.getProperties());
+
+ // delete existing children and create new ones
+ deleteChildrenNotInCollection(configResourceParent, data);
+ for (ConfigurationPersistData item : data.getItems()) {
+ String path = configResourceParent.getPath() + "/" + item.getCollectionItemName();
+ getOrCreateResource(resourceResolver, path, item.getProperties());
+ }
+
+ commit(resourceResolver, configResourceCollectionParentPath);
+ return true;
+ }
+
+ @Override
+ public boolean deleteConfiguration(@NotNull ResourceResolver resourceResolver, @NotNull String configResourcePath) {
+ if (!config.enabled()) {
+ return false;
+ }
+ Resource resource = resourceResolver.getResource(configResourcePath);
+ if (resource != null) {
+ try {
+ log.trace("! Delete resource {}", resource.getPath());
+ resourceResolver.delete(resource);
+ }
+ catch (PersistenceException ex) {
+ throw convertPersistenceException("Unable to delete configuration at " + configResourcePath, ex);
+ }
+ }
+ commit(resourceResolver, configResourcePath);
+ return true;
+ }
+
+ private Resource getOrCreateResource(ResourceResolver resourceResolver, String path, Map properties) {
+ try {
+ Resource resource = ResourceUtil.getOrCreateResource(resourceResolver, path, (String)null, (String)null, false);
+ if (properties != null) {
+ replaceProperties(resource, properties);
+ }
+ return resource;
+ }
+ catch (PersistenceException ex) {
+ throw convertPersistenceException("Unable to persist configuration to " + path, ex);
+ }
+ }
+
+ /**
+ * Delete children that are no longer contained in list of collection items.
+ * @param resource Parent resource
+ * @param data List of collection items
+ */
+ private void deleteChildrenNotInCollection(Resource resource, ConfigurationCollectionPersistData data) {
+ ResourceResolver resourceResolver = resource.getResourceResolver();
+
+ Set collectionItemNames = new HashSet<>();
+ for (ConfigurationPersistData item : data.getItems()) {
+ collectionItemNames.add(item.getCollectionItemName());
+ }
+
+ try {
+ for (Resource child : resource.getChildren()) {
+ if (!collectionItemNames.contains(child.getName())) {
+ log.trace("! Delete resource {}", child.getPath());
+ resourceResolver.delete(child);
+ }
+ }
+ }
+ catch (PersistenceException ex) {
+ throw convertPersistenceException("Unable to remove children from " + resource.getPath(), ex);
+ }
+ }
+
+ private void replaceProperties(Resource resource, Map properties) {
+ if (log.isTraceEnabled()) {
+ log.trace("! Store properties for resource {}: {}", resource.getPath(), MapUtil.traceOutput(properties));
+ }
+ ModifiableValueMap modValueMap = resource.adaptTo(ModifiableValueMap.class);
+ if (modValueMap == null) {
+ throw new ConfigurationPersistenceAccessDeniedException("No write access: Unable to store configuration data to " + resource.getPath() + ".");
+ }
+ // remove all existing properties that are not filterd
+ Set propertyNamesToRemove = new HashSet<>(modValueMap.keySet());
+ PropertiesFilterUtil.removeIgnoredProperties(propertyNamesToRemove, configurationManagementSettings);
+ for (String propertyName : propertyNamesToRemove) {
+ modValueMap.remove(propertyName);
+ }
+ modValueMap.putAll(properties);
+ }
+
+ private void commit(ResourceResolver resourceResolver, String relatedResourcePath) {
+ try {
+ resourceResolver.commit();
+ }
+ catch (PersistenceException ex) {
+ throw convertPersistenceException("Unable to persist configuration changes to " + relatedResourcePath, ex);
+ }
+ }
+
+ private ConfigurationPersistenceException convertPersistenceException(String message, PersistenceException ex) {
+ if (StringUtils.equals(ex.getCause().getClass().getName(), "javax.jcr.AccessDeniedException")) {
+ // detect if commit failed due to read-only access to repository
+ return new ConfigurationPersistenceAccessDeniedException("No write access: " + message, ex);
+ }
+ return new ConfigurationPersistenceException(message, ex);
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/AnnotationClassConfigurationMetadataProvider.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/AnnotationClassConfigurationMetadataProvider.java
new file mode 100644
index 000000000..28ccc1413
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/AnnotationClassConfigurationMetadataProvider.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.metadata;
+
+import java.util.Collections;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+import org.apache.sling.caconfig.spi.ConfigurationMetadataProvider;
+import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.util.tracker.BundleTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Detects configuration annotation classes deployed by any bundle via OSGi extender pattern.
+ */
+@Component(immediate = true, service = ConfigurationMetadataProvider.class)
+public class AnnotationClassConfigurationMetadataProvider implements ConfigurationMetadataProvider {
+
+ private BundleTracker bundleTracker;
+ private ConcurrentSkipListMap bundleMappings = new ConcurrentSkipListMap<>();
+
+ private static final Logger log = LoggerFactory.getLogger(AnnotationClassConfigurationMetadataProvider.class);
+
+ @Activate
+ private void activate(BundleContext bundleContext) {
+ ConfigClassBundleTackerCustomizer bundlerTrackerCustomizer = new ConfigClassBundleTackerCustomizer(this);
+ bundleTracker = new BundleTracker(bundleContext, Bundle.ACTIVE, bundlerTrackerCustomizer);
+ bundleTracker.open();
+ }
+
+ @Deactivate
+ private void deactivate() {
+ bundleTracker.close();
+ bundleTracker = null;
+ }
+
+ @Override
+ public @NotNull SortedSet getConfigurationNames() {
+ SortedSet allConfigNames = new TreeSet();
+ for (BundleConfigurationMapping bundleMapping : bundleMappings.values()) {
+ allConfigNames.addAll(bundleMapping.getConfigurationNames());
+ }
+ return Collections.unmodifiableSortedSet(allConfigNames);
+ }
+
+ @Override
+ public ConfigurationMetadata getConfigurationMetadata(String configName) {
+ ConfigurationMapping mapping = getConfigurationMapping(configName);
+ if (mapping != null) {
+ return mapping.getConfigMetadata();
+ }
+ else {
+ return null;
+ }
+ }
+
+ /**
+ * Get configuration mapping for given config name.
+ * On the way check for config name mapping conflicts accross bundles and log a warning if found.
+ * Is difficult to do this beforehand due to the lazy initialization of the bundle config mappings and the
+ * dynamic behavior of coming and going bundles with configuration classes.
+ * @param configName Configuration name
+ * @return Configuration mapping or null if none found
+ */
+ @SuppressWarnings("null")
+ ConfigurationMapping getConfigurationMapping(String configName) {
+ ConfigurationMapping matchingConfigMapping = null;
+ BundleConfigurationMapping matchingBundleMapping = null;
+ for (BundleConfigurationMapping bundleMapping : bundleMappings.values()) {
+ ConfigurationMapping configMapping = bundleMapping.getConfigurationMapping(configName);
+ if (configMapping != null) {
+ if (matchingConfigMapping == null) {
+ matchingConfigMapping = configMapping;
+ matchingBundleMapping = bundleMapping;
+ }
+ else {
+ // conflict in name mapping across bundles found
+ log.warn("Configuration name conflict: Both configuration classes {} (Bundle {}) "
+ + "and {} (Bundle {}) define the configuration name '{}', ignoring the latter.",
+ matchingConfigMapping.getConfigClass().getName(),
+ matchingBundleMapping.getBundle().getSymbolicName(),
+ configMapping.getConfigClass().getName(),
+ bundleMapping.getBundle().getSymbolicName(),
+ configName);
+ }
+ }
+ }
+ return matchingConfigMapping;
+ }
+
+ void addBundeMapping(BundleConfigurationMapping bundleMapping) {
+ log.debug("Add bundle mapping: {}", bundleMapping);
+ bundleMappings.put(bundleMapping.getBundle(), bundleMapping);
+ }
+
+ void removeBundleMapping(BundleConfigurationMapping bundleMapping) {
+ log.debug("Remove bundle mapping: {}", bundleMapping);
+ bundleMappings.remove(bundleMapping.getBundle());
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/AnnotationClassParser.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/AnnotationClassParser.java
new file mode 100644
index 000000000..38cfb7fe7
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/AnnotationClassParser.java
@@ -0,0 +1,228 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.metadata;
+
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.caconfig.annotation.Configuration;
+import org.apache.sling.caconfig.annotation.Property;
+import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata;
+import org.apache.sling.caconfig.spi.metadata.PropertyMetadata;
+
+/**
+ * Helper methods for parsing metadata from configuration annotation classes.
+ */
+public final class AnnotationClassParser {
+
+ private static final Pattern METHOD_NAME_MAPPING = Pattern.compile("(\\$\\$)|(\\$)|(__)|(_)");
+
+ private AnnotationClassParser() {
+ // static methods only
+ }
+
+ /**
+ * Checks if the given class is suitable to be mapped with context-aware configuration.
+ * The given class has to be an annotation class, and the {@link Configuration} annotation has to be present.
+ * @param clazz Given class
+ * @return True if class is suitable for context-aware configuration
+ */
+ public static boolean isContextAwareConfig(Class> clazz) {
+ return clazz.isAnnotation() && clazz.isAnnotationPresent(Configuration.class);
+ }
+
+ /**
+ * Get configuration name for given configuration annotation class.
+ * @param clazz Annotation class
+ * @return Configuration name
+ */
+ public static String getConfigurationName(Class> clazz) {
+ Configuration configAnnotation = clazz.getAnnotation(Configuration.class);
+ if (configAnnotation == null) {
+ return null;
+ }
+ return getConfigurationName(clazz, configAnnotation);
+ }
+
+ /**
+ * Get configuration name for given configuration annotation class.
+ * @param clazz Annotation class
+ * @param configAnnotation Configuration metadata
+ * @return Configuration name
+ */
+ private static String getConfigurationName(Class> clazz, Configuration configAnnotation) {
+ String configName = configAnnotation.name();
+ if (StringUtils.isBlank(configName)) {
+ configName = clazz.getName();
+ }
+ return configName;
+ }
+
+ /**
+ * Implements the method name mapping as defined in OSGi R6 Compendium specification,
+ * Chapter 112. Declarative Services Specification, Chapter 112.8.2.1. Component Property Mapping.
+ * @param methodName Method name
+ * @return Mapped property name
+ */
+ public static String getPropertyName(String methodName) {
+ Matcher matcher = METHOD_NAME_MAPPING.matcher(methodName);
+ StringBuffer mappedName = new StringBuffer();
+ while (matcher.find()) {
+ String replacement = "";
+ if (matcher.group(1) != null) {
+ replacement = "\\$";
+ }
+ if (matcher.group(2) != null) {
+ replacement = "";
+ }
+ if (matcher.group(3) != null) {
+ replacement = "_";
+ }
+ if (matcher.group(4) != null) {
+ replacement = ".";
+ }
+ matcher.appendReplacement(mappedName, replacement);
+ }
+ matcher.appendTail(mappedName);
+ return mappedName.toString();
+ }
+
+ /**
+ * Build configuration metadata by parsing the given annotation interface class and it's configuration annotations.
+ * @param clazz Configuration annotation class
+ * @return Configuration metadata
+ */
+ public static ConfigurationMetadata buildConfigurationMetadata(Class> clazz) {
+ Configuration configAnnotation = clazz.getAnnotation(Configuration.class);
+ if (configAnnotation == null) {
+ throw new IllegalArgumentException("Class has not @Configuration annotation: " + clazz.getName());
+ }
+
+ // configuration metadata and property metadata
+ String configName = getConfigurationName(clazz, configAnnotation);
+ ConfigurationMetadata configMetadata = new ConfigurationMetadata(configName,
+ buildConfigurationMetadata_PropertyMetadata(clazz),
+ configAnnotation.collection())
+ .label(emptyToNull(configAnnotation.label()))
+ .description(emptyToNull(configAnnotation.description()))
+ .properties(propsArrayToMap(configAnnotation.property()));
+
+ return configMetadata;
+ }
+
+ /**
+ * Build configuration metadata by parsing the given annotation interface class which is used for nested configurations.
+ * @param clazz Configuration annotation class
+ * @return Configuration metadata
+ */
+ private static ConfigurationMetadata buildConfigurationMetadata_Nested(Class> clazz, String configName, boolean collection) {
+ return new ConfigurationMetadata(configName,
+ buildConfigurationMetadata_PropertyMetadata(clazz),
+ collection);
+ }
+
+ private static Collection> buildConfigurationMetadata_PropertyMetadata(Class> clazz) {
+ // sort properties by order number, or alternatively by label, name
+ SortedSet> propertyMetadataSet = new TreeSet<>(new Comparator>() {
+ @Override
+ public int compare(PropertyMetadata> o1, PropertyMetadata> o2) {
+ int compare = Integer.compare(o1.getOrder(), o2.getOrder());
+ if (compare == 0) {
+ String sort1 = StringUtils.defaultString(o1.getLabel(), o1.getName());
+ String sort2 = StringUtils.defaultString(o2.getLabel(), o2.getName());
+ compare = sort1.compareTo(sort2);
+ }
+ return compare;
+ }
+ });
+ Method[] propertyMethods = clazz.getDeclaredMethods();
+ for (Method propertyMethod : propertyMethods) {
+ PropertyMetadata> propertyMetadata = buildPropertyMetadata(propertyMethod, propertyMethod.getReturnType());
+ propertyMetadataSet.add(propertyMetadata);
+ }
+ return propertyMetadataSet;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static PropertyMetadata buildPropertyMetadata(Method propertyMethod, Class type) {
+ String propertyName = getPropertyName(propertyMethod.getName());
+
+ PropertyMetadata> propertyMetadata;
+ if (type.isArray() && type.getComponentType().isAnnotation()) {
+ ConfigurationMetadata nestedConfigMetadata = buildConfigurationMetadata_Nested(type.getComponentType(), propertyName, true);
+ propertyMetadata = new PropertyMetadata<>(propertyName, ConfigurationMetadata[].class)
+ .configurationMetadata(nestedConfigMetadata);
+ }
+ else if (type.isAnnotation()) {
+ ConfigurationMetadata nestedConfigMetadata = buildConfigurationMetadata_Nested(type, propertyName, false);
+ propertyMetadata = new PropertyMetadata<>(propertyName, ConfigurationMetadata.class)
+ .configurationMetadata(nestedConfigMetadata);
+ }
+ else {
+ propertyMetadata = new PropertyMetadata<>(propertyName, type)
+ .defaultValue((T)propertyMethod.getDefaultValue());
+ }
+
+ Property propertyAnnotation = propertyMethod.getAnnotation(Property.class);
+ if (propertyAnnotation != null) {
+ propertyMetadata.label(emptyToNull(propertyAnnotation.label()))
+ .description(emptyToNull(propertyAnnotation.description()))
+ .properties(propsArrayToMap(propertyAnnotation.property()))
+ .order(propertyAnnotation.order());
+ }
+ else {
+ Map emptyMap = Collections.emptyMap();
+ propertyMetadata.properties(emptyMap);
+ }
+
+ return (PropertyMetadata)propertyMetadata;
+ }
+
+ private static String emptyToNull(String value) {
+ if (StringUtils.isEmpty(value)) {
+ return null;
+ }
+ else {
+ return value;
+ }
+ }
+
+ private static Map propsArrayToMap(String[] properties) {
+ Map props = new HashMap<>();
+ for (String property : properties) {
+ int index = StringUtils.indexOf(property, "=");
+ if (index >= 0) {
+ String key = property.substring(0, index);
+ String value = property.substring(index + 1);
+ props.put(key, value);
+ }
+ }
+ return props;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/BundleConfigurationMapping.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/BundleConfigurationMapping.java
new file mode 100644
index 000000000..20480290a
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/BundleConfigurationMapping.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.metadata;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.commons.lang3.StringUtils;
+import org.osgi.framework.Bundle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Contains metadata of all configuration classes that where declared via the bundle header for a bundle.
+ * The configuration class metadata is not parsed on initialization, but lazily on first access.
+ */
+class BundleConfigurationMapping {
+
+ private final Bundle bundle;
+ private final String classNamesList;
+ private final AtomicReference> configMappingsRef = new AtomicReference<>(null);
+
+ private static final Logger log = LoggerFactory.getLogger(BundleConfigurationMapping.class);
+
+ public BundleConfigurationMapping(Bundle bundle, String classNamesList) {
+ this.bundle = bundle;
+ this.classNamesList = classNamesList;
+ }
+
+ public Bundle getBundle() {
+ return bundle;
+ }
+
+ /**
+ * Thread-safe lazy initialization of configuration mappings.
+ * @return Configuration mappings
+ */
+ private Map getConfigMappings() {
+ Map configMappings = configMappingsRef.get();
+ if (configMappings == null) {
+ configMappings = initializeConfigMappings();
+ if (configMappingsRef.compareAndSet(null, configMappings)) {
+ return configMappings;
+ }
+ else {
+ return configMappingsRef.get();
+ }
+ }
+ else {
+ return configMappings;
+ }
+ }
+
+ /**
+ * Parse all annotation classes
+ * @return
+ */
+ private Map initializeConfigMappings() {
+ Map configMappings = new HashMap<>();
+
+ String[] classNames = StringUtils.split(StringUtils.deleteWhitespace(classNamesList), ",");
+ for (String className : classNames) {
+ try {
+ Class> configClass = bundle.loadClass(className);
+ if (AnnotationClassParser.isContextAwareConfig(configClass)) {
+ log.debug("{}: Add configuration class {}", bundle.getSymbolicName(), className);
+
+ ConfigurationMapping configMapping = new ConfigurationMapping(configClass);
+ if (!hasMappingConflict(configMapping, configMappings)) {
+ configMappings.put(configMapping.getConfigName(), configMapping);
+ }
+ }
+ else {
+ log.warn("Ignoring invalid configuration class: {}", className);
+ }
+ }
+ catch (ClassNotFoundException ex) {
+ log.warn("Unable to load class: " + className, ex);
+ }
+ }
+
+ return configMappings;
+ }
+
+ private boolean hasMappingConflict(ConfigurationMapping newConfigMapping,
+ Map configMappings) {
+ ConfigurationMapping conflictingConfigMapping = configMappings.get(newConfigMapping.getConfigName());
+ if (conflictingConfigMapping != null) {
+ log.warn("Configuration name conflict in bundle {}: Both configuration classes {} and {} define the configuration name '{}', ignoring the latter.",
+ bundle.getSymbolicName(),
+ conflictingConfigMapping.getConfigClass().getName(),
+ newConfigMapping.getConfigClass().getName(),
+ newConfigMapping.getConfigName());
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ public Set getConfigurationNames() {
+ return getConfigMappings().keySet();
+ }
+
+ public ConfigurationMapping getConfigurationMapping(String configName) {
+ return getConfigMappings().get(configName);
+ }
+
+ @Override
+ public String toString() {
+ return "Classes from bundle '" + bundle.getSymbolicName() + "': " + classNamesList;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/ConfigClassBundleTackerCustomizer.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/ConfigClassBundleTackerCustomizer.java
new file mode 100644
index 000000000..fd1063810
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/ConfigClassBundleTackerCustomizer.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.metadata;
+
+import static org.apache.sling.caconfig.impl.ConfigurationNameConstants.CONFIGURATION_CLASSES_HEADER;
+
+import java.util.Dictionary;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleEvent;
+import org.osgi.util.tracker.BundleTrackerCustomizer;
+
+/**
+ * Detects deployed bundles with configuration class header.
+ */
+class ConfigClassBundleTackerCustomizer implements BundleTrackerCustomizer {
+
+ private final AnnotationClassConfigurationMetadataProvider metadataProvider;
+
+ public ConfigClassBundleTackerCustomizer(AnnotationClassConfigurationMetadataProvider metadataProvider) {
+ this.metadataProvider = metadataProvider;
+ }
+
+ @Override
+ public BundleConfigurationMapping addingBundle(Bundle bundle, BundleEvent event) {
+ Dictionary headers = bundle.getHeaders();
+ String classeNamesList = headers.get(CONFIGURATION_CLASSES_HEADER);
+ if (classeNamesList == null) {
+ return null;
+ }
+ BundleConfigurationMapping bundleMapping = new BundleConfigurationMapping(bundle, classeNamesList);
+ metadataProvider.addBundeMapping(bundleMapping);
+ return bundleMapping;
+ }
+
+ @Override
+ public void modifiedBundle(Bundle bundle, BundleEvent event, BundleConfigurationMapping bundleMapping) {
+ // nothing to do
+ }
+
+ @Override
+ public void removedBundle(Bundle bundle, BundleEvent event, BundleConfigurationMapping bundleMapping) {
+ metadataProvider.removeBundleMapping(bundleMapping);
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/ConfigurationMapping.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/ConfigurationMapping.java
new file mode 100644
index 000000000..13e856636
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/ConfigurationMapping.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.metadata;
+
+import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata;
+
+/**
+ * Contains mapping of configuration class to metadata parsed from it's fields and annotations.
+ */
+class ConfigurationMapping {
+
+ private final Class> configClass;
+ private final ConfigurationMetadata configMetadata;
+
+ public ConfigurationMapping(Class> configClass) {
+ this.configClass = configClass;
+ this.configMetadata = AnnotationClassParser.buildConfigurationMetadata(configClass);
+ }
+
+ public Class> getConfigClass() {
+ return configClass;
+ }
+
+ public String getConfigName() {
+ return configMetadata.getName();
+ }
+
+ public ConfigurationMetadata getConfigMetadata() {
+ return configMetadata;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/ConfigurationMetadataProviderMultiplexerImpl.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/ConfigurationMetadataProviderMultiplexerImpl.java
new file mode 100644
index 000000000..dd76471aa
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/metadata/ConfigurationMetadataProviderMultiplexerImpl.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.metadata;
+
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationMetadataProviderMultiplexer;
+import org.apache.sling.caconfig.spi.ConfigurationMetadataProvider;
+import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata;
+import org.apache.sling.commons.osgi.Order;
+import org.apache.sling.commons.osgi.RankedServices;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+
+/**
+ * Detects all {@link ConfigurationMetadataProvider} implementations in the container
+ * and consolidates their result based on service ranking.
+ */
+@Component(service = ConfigurationMetadataProviderMultiplexer.class,
+reference={
+ @Reference(name="configurationMetadataProvider", service=ConfigurationMetadataProvider.class,
+ bind="bindConfigurationMetadataProvider", unbind="unbindConfigurationMetadataProvider",
+ cardinality=ReferenceCardinality.MULTIPLE,
+ policy=ReferencePolicy.DYNAMIC, policyOption=ReferencePolicyOption.GREEDY)
+})
+public class ConfigurationMetadataProviderMultiplexerImpl implements ConfigurationMetadataProviderMultiplexer {
+
+ private RankedServices items = new RankedServices<>(Order.DESCENDING);
+
+ protected void bindConfigurationMetadataProvider(ConfigurationMetadataProvider configurationMetadataProvider, Map props) {
+ items.bind(configurationMetadataProvider, props);
+ }
+
+ protected void unbindConfigurationMetadataProvider(ConfigurationMetadataProvider configurationMetadataProvider, Map props) {
+ items.unbind(configurationMetadataProvider, props);
+ }
+
+ /**
+ * Merge configuration names from all providers.
+ */
+ @Override
+ public @NotNull SortedSet getConfigurationNames() {
+ SortedSet configNames = new TreeSet<>();
+ for (ConfigurationMetadataProvider item : items) {
+ configNames.addAll(item.getConfigurationNames());
+ }
+ return configNames;
+ }
+
+ /**
+ * Get configuration metadata from first provider (ranking priority) that has an answer.
+ */
+ @Override
+ public ConfigurationMetadata getConfigurationMetadata(String configName) {
+ for (ConfigurationMetadataProvider item : items) {
+ ConfigurationMetadata configMetadata = item.getConfigurationMetadata(configName);
+ if (configMetadata != null) {
+ return configMetadata;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/ConfigurationOverrideMultiplexerImpl.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/ConfigurationOverrideMultiplexerImpl.java
new file mode 100644
index 000000000..c788e5838
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/ConfigurationOverrideMultiplexerImpl.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.override;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.SyntheticResource;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.caconfig.impl.ConfigurationResourceWrapper;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationOverrideMultiplexer;
+import org.apache.sling.caconfig.resource.impl.util.MapUtil;
+import org.apache.sling.caconfig.spi.ConfigurationOverrideProvider;
+import org.apache.sling.commons.osgi.Order;
+import org.apache.sling.commons.osgi.RankedServices;
+import org.apache.sling.commons.osgi.RankedServices.ChangeListener;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Detects all {@link ConfigurationOverrideProvider} implementations in the container
+ * and consolidates their result based on service ranking.
+ */
+@Component(service = ConfigurationOverrideMultiplexer.class,
+reference={
+ @Reference(name="configurationOverrideProvider", service=ConfigurationOverrideProvider.class,
+ bind="bindConfigurationOverrideProvider", unbind="unbindConfigurationOverrideProvider",
+ cardinality=ReferenceCardinality.MULTIPLE,
+ policy=ReferencePolicy.DYNAMIC, policyOption=ReferencePolicyOption.GREEDY)
+})
+public class ConfigurationOverrideMultiplexerImpl implements ConfigurationOverrideMultiplexer, ChangeListener {
+
+ private RankedServices items = new RankedServices<>(Order.DESCENDING, this);
+ private volatile Collection allOverrides = Collections.emptyList();
+
+ private static final Logger log = LoggerFactory.getLogger(ConfigurationOverrideMultiplexerImpl.class);
+
+ protected void bindConfigurationOverrideProvider(ConfigurationOverrideProvider item, Map props) {
+ items.bind(item, props);
+ }
+
+ protected void unbindConfigurationOverrideProvider(ConfigurationOverrideProvider item, Map props) {
+ items.unbind(item, props);
+ }
+
+ @Override
+ public boolean isAllOverridden(@NotNull String contextPath, @NotNull String configName) {
+ for (OverrideItem override : allOverrides) {
+ if (StringUtils.equals(configName, override.getConfigName()) && override.matchesPath(contextPath)) {
+ if (override.isAllProperties()) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Map overrideProperties(@NotNull String contextPath, @NotNull String configName, @NotNull Map properties) {
+ if (allOverrides.size() == 0) {
+ return null;
+ }
+ boolean anyMatch = false;
+ Map overrideProperties = new HashMap<>(properties);
+
+ for (OverrideItem override : allOverrides) {
+ if (StringUtils.equals(configName, override.getConfigName()) && override.matchesPath(contextPath)) {
+ if (override.isAllProperties()) {
+ overrideProperties.clear();
+ }
+ overrideProperties.putAll(override.getProperties());
+ anyMatch = true;
+ }
+ }
+
+ if (anyMatch) {
+ return overrideProperties;
+ }
+ else {
+ return null;
+ }
+ }
+
+ @Override
+ public Resource overrideProperties(@NotNull String contextPath, @NotNull String configName, @Nullable Resource configResource) {
+ if (configResource == null) {
+ return null;
+ }
+ return overrideProperties(contextPath, configName, configResource, configResource.getResourceResolver());
+ }
+
+ @Override
+ public Resource overrideProperties(@NotNull String contextPath, @NotNull String configName, @Nullable Resource configResource, @NotNull ResourceResolver resourceResolver) {
+ Map overrideProperties = overrideProperties(contextPath, configName, configResource != null ? configResource.getValueMap() : ValueMap.EMPTY);
+ if (overrideProperties == null) {
+ return configResource;
+ }
+ Resource configResourceToUse = configResource;
+ if (configResourceToUse == null) {
+ // build synthetic resource if override properties exist
+ configResourceToUse = new SyntheticResource(resourceResolver, (String)null, (String)null);
+ }
+ if (log.isTraceEnabled()) {
+ log.trace("! Override properties for context path " + contextPath + ", name '" + configName + "', "
+ + (configResource != null ? "config path " + configResource.getPath() : "no config path") + ": "
+ + (configResource != null ? MapUtil.traceOutput(configResource.getValueMap()) : "empty") + " -> " + MapUtil.traceOutput(overrideProperties));
+ }
+ return new ConfigurationResourceWrapper(configResourceToUse, new ValueMapDecorator(overrideProperties));
+ }
+
+ /**
+ * If a provider is added or removed parse and collect all overrides again (to ensure correct overall order is preserved).
+ */
+ @Override
+ public void changed() {
+ List overrides = new ArrayList<>();
+ for (ConfigurationOverrideProvider item : items) {
+ Collection itemOverrides = OverrideStringParser.parse(item.getOverrideStrings());
+ if (log.isDebugEnabled() && !itemOverrides.isEmpty()) {
+ log.debug("Override items from " + item.getClass().getName() + ":\n" + StringUtils.join(itemOverrides, "\n"));
+ }
+ overrides.addAll(itemOverrides);
+ }
+ allOverrides = overrides;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/OsgiConfigurationOverrideProvider.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/OsgiConfigurationOverrideProvider.java
new file mode 100644
index 000000000..eb18d11f8
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/OsgiConfigurationOverrideProvider.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.override;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.sling.caconfig.spi.ConfigurationOverrideProvider;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+/**
+ * Provides parameter override map from OSGi factory configuration.
+ */
+@Component(service = ConfigurationOverrideProvider.class, immediate = true)
+@Designate(ocd = OsgiConfigurationOverrideProvider.Config.class, factory = true)
+public final class OsgiConfigurationOverrideProvider implements ConfigurationOverrideProvider {
+
+ @ObjectClassDefinition(name = "Apache Sling Context-Aware Configuration Override Provider: OSGi configuration",
+ description = "Allows to override configuration property values from OSGi configurations.")
+ public static @interface Config {
+
+ @AttributeDefinition(name = "Description",
+ description = "This description is used for display in the web console.")
+ String description();
+
+ @AttributeDefinition(name = "Overrides",
+ description = "Override strings - examples:\n"
+ + "{configName}/{propertyName}={propertyJsonValue}\n"
+ + "{configName}={propertyJsonObject}\n"
+ + "[{contextPath}]{configName}/{propertyName}={propertyJsonValue}\n"
+ + "[{contextPath}]{configName}={propertyJsonObject}")
+ String[] overrides();
+
+ @AttributeDefinition(name = "Enabled",
+ description = "Enable this override provider.")
+ boolean enabled() default false;
+
+ @AttributeDefinition(name = "Service Ranking",
+ description = "Priority of configuration override providers (higher = higher priority).")
+ int service_ranking() default 100;
+
+ String webconsole_configurationFactory_nameHint() default "{description}, enabled={enabled}";
+
+ }
+
+ private Collection overrideStrings;
+
+ @Activate
+ void activate(Config config) {
+ List overrides = new ArrayList<>();
+ if (config.enabled()) {
+ overrides.addAll(Arrays.asList(config.overrides()));
+ }
+ this.overrideStrings = overrides;
+ }
+
+ @Override
+ public @NotNull Collection getOverrideStrings() {
+ return overrideStrings;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/OverrideItem.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/OverrideItem.java
new file mode 100644
index 000000000..6ff8c31b8
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/OverrideItem.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.override;
+
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+
+/**
+ * Holds override information provided by override providers.
+ */
+class OverrideItem {
+
+ private final String path;
+ private final Pattern pathPattern;
+ private final String configName;
+ private final Map properties;
+ private final boolean allProperties;
+
+ public OverrideItem(String path, String configName,
+ Map properties, boolean allProperties) {
+ this.path = path;
+ this.pathPattern = toPathPattern(path);
+ this.configName = configName;
+ this.properties = properties;
+ this.allProperties = allProperties;
+ }
+
+ private static Pattern toPathPattern(String path) {
+ if (StringUtils.isBlank(path)) {
+ return null;
+ }
+ return Pattern.compile("^" + Pattern.quote(StringUtils.trim(path)) + "(/.*)?$");
+ }
+
+ /**
+ * @return Path (incl. subtree) to match - or null for all paths
+ */
+ public String getPath() {
+ return path;
+ }
+
+ /**
+ * @param path Path to check
+ * @return true if path matches
+ */
+ public boolean matchesPath(String path) {
+ if (pathPattern == null) {
+ return true;
+ }
+ else {
+ return pathPattern.matcher(path).matches();
+ }
+ }
+
+ /**
+ * @return Configuration name (may contain a relative hierarchy with "/")
+ */
+ public String getConfigName() {
+ return configName;
+ }
+
+ /**
+ * @return Properties map
+ */
+ public Map getProperties() {
+ return properties;
+ }
+
+ /**
+ * @return If true, all properties for this config name should be replaced
+ * with those from the map. Otherwise they are merged.
+ */
+ public boolean isAllProperties() {
+ return allProperties;
+ }
+
+ @Override
+ public String toString() {
+ return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/OverrideStringParser.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/OverrideStringParser.java
new file mode 100644
index 000000000..57dc0b9e3
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/OverrideStringParser.java
@@ -0,0 +1,288 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.override;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonException;
+import javax.json.JsonNumber;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import javax.json.JsonReaderFactory;
+import javax.json.JsonString;
+import javax.json.JsonValue;
+
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.caconfig.spi.metadata.PropertyMetadata;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Parses override configuration strings like these:
+ *
+ * {configName}/{propertyName}={propertyJsonValue}
+ * {configName}={propertyJsonObject}
+ * [{contextPath}]{configName}/{propertyName}={propertyJsonValue}
+ * [{contextPath}]{configName}={propertyJsonObject}
+ *
+ */
+class OverrideStringParser {
+
+ private static final Logger log = LoggerFactory.getLogger(OverrideStringParser.class);
+
+ private static final Pattern OVERRIDE_PATTERN = Pattern.compile("^(\\[([^\\[\\]=]+)\\])?([^\\[\\]=]+)=(.*)$");
+
+ private static final JsonReaderFactory JSON_READER_FACTORY = Json.createReaderFactory(Collections.emptyMap());
+
+ private OverrideStringParser() {
+ // static method sonly
+ }
+
+ /**
+ * Parses a list of override strings from a override provider.
+ * @param overrideStrings Override strings
+ * @return Override objects
+ */
+ public static Collection parse(Collection overrideStrings) {
+ List result = new ArrayList<>();
+
+ for (String overrideString : overrideStrings) {
+
+ // check if override generic pattern is matched
+ Matcher matcher = OVERRIDE_PATTERN.matcher(StringUtils.defaultString(overrideString));
+ if (!matcher.matches()) {
+ log.warn("Ignore config override string - invalid syntax: {}", overrideString);
+ continue;
+ }
+
+ // get single parts
+ String path = StringUtils.trim(matcher.group(2));
+ String configName = StringUtils.trim(matcher.group(3));
+ String value = StringUtils.trim(StringUtils.defaultString(matcher.group(4)));
+
+ OverrideItem item;
+ try {
+ // check if value is JSON = defines whole parameter map for a config name
+ JsonObject json = toJson(value);
+ if (json != null) {
+ item = new OverrideItem(path, configName, toMap(json), true);
+ }
+ else {
+ // otherwise it defines a key/value pair in a single line
+ String propertyName = StringUtils.substringAfterLast(configName, "/");
+ if (StringUtils.isEmpty(propertyName)) {
+ log.warn("Ignore config override string - missing property name: {}", overrideString);
+ continue;
+ }
+ configName = StringUtils.substringBeforeLast(configName, "/");
+ Map props = new HashMap<>();
+ props.put(propertyName, convertJsonValue(value));
+ item = new OverrideItem(path, configName, props, false);
+ }
+ }
+ catch (JsonException ex) {
+ log.warn("Ignore config override string - invalid JSON syntax ({}): {}", ex.getMessage(), overrideString);
+ continue;
+ }
+
+ // validate item
+ if (!isValid(item, overrideString)) {
+ continue;
+ }
+
+ // if item does not contain a full property set try to merge with existing one
+ if (!item.isAllProperties()) {
+ boolean foundMatchingItem = false;
+ for (OverrideItem existingItem : result) {
+ if (!existingItem.isAllProperties()
+ && StringUtils.equals(item.getPath(), existingItem.getPath())
+ && StringUtils.equals(item.getConfigName(), existingItem.getConfigName())) {
+ existingItem.getProperties().putAll(item.getProperties());
+ foundMatchingItem = true;
+ break;
+ }
+ }
+ if (foundMatchingItem) {
+ continue;
+ }
+ }
+
+ // add item to result
+ result.add(item);
+ }
+
+ return result;
+ }
+
+ /**
+ * Try to convert value to JSON object
+ * @param value Value string
+ * @return JSON object or null if the string does not start with "{"
+ * @throws JSONException when JSON parsing failed
+ */
+ private static JsonObject toJson(String value) {
+ if (!StringUtils.startsWith(value, "{")) {
+ return null;
+ }
+ try (Reader reader = new StringReader(value);
+ JsonReader jsonReader = JSON_READER_FACTORY.createReader(reader)) {
+ return jsonReader.readObject();
+ }
+ catch (IOException ex) {
+ return null;
+ }
+ }
+
+ /**
+ * Convert JSON object to map.
+ * @param json JSON object
+ * @return Map (keys/values are not validated)
+ */
+ private static Map toMap(JsonObject json) {
+ Map props = new HashMap<>();
+ Iterator keys = json.keySet().iterator();
+ while (keys.hasNext()) {
+ String key = keys.next();
+ props.put(key, convertJsonValue(json.get(key)));
+ }
+ return props;
+ }
+
+ /**
+ * Convert single JSON-conformant value object
+ * @param jsonValue JSON value
+ * @return Object
+ * @throws JSONException If JSON-parsing of value failed
+ */
+ private static Object convertJsonValue(String jsonValue) {
+ String jsonString = "{\"value\":" + jsonValue + "}";
+ JsonObject json = toJson(jsonString);
+ return convertJsonValue(json.get("value"));
+ }
+
+ private static Object convertJsonValue(JsonValue jsonValue) {
+ switch (jsonValue.getValueType()) {
+ case STRING:
+ return ((JsonString)jsonValue).getString();
+ case NUMBER:
+ JsonNumber number = (JsonNumber)jsonValue;
+ if (number.isIntegral()) {
+ return number.longValue();
+ }
+ else {
+ return number.doubleValue();
+ }
+ case TRUE:
+ return true;
+ case FALSE:
+ return false;
+ case NULL:
+ return null;
+ case ARRAY:
+ return convertJsonArray((JsonArray)jsonValue);
+ default:
+ throw new RuntimeException("Unexpected JSON value type: " + jsonValue.getValueType() + ": " + jsonValue);
+ }
+ }
+
+ private static Object convertJsonArray(JsonArray jsonArray) {
+ if (jsonArray.size() > 0) {
+ Object firstValue = convertJsonValue(jsonArray.get(0));
+ if (firstValue != null) {
+ Class firstType = firstValue.getClass();
+ Object convertedArray = Array.newInstance(firstType, jsonArray.size());
+ for (int i=0; i entry : item.getProperties().entrySet()) {
+ String propertyName = entry.getKey();
+ if (StringUtils.isEmpty(propertyName) || StringUtils.contains(propertyName, "/")) {
+ log.warn("Ignore config override string - invalid property name ({}): {}", propertyName, overrideString);
+ return false;
+ }
+ Object value = entry.getValue();
+ if (value == null || !isSupportedType(value)) {
+ log.warn("Ignore config override string - invalid property value ({} - {}): {}", value, value != null ? value.getClass().getName() : "", overrideString);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Validate if the given object is not null, and the type is supported for configuration values.
+ * @param value Value
+ * @return true if valid
+ */
+ private static boolean isSupportedType(Object value) {
+ if (value == null) {
+ return false;
+ }
+ Class clazz = value.getClass();
+ if (clazz.isArray()) {
+ clazz = clazz.getComponentType();
+ }
+ for (Class type : PropertyMetadata.SUPPORTED_TYPES) {
+ if (type.equals(clazz )) {
+ return true;
+ }
+ if (type.isPrimitive() && ClassUtils.primitiveToWrapper(type).equals(clazz)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/SystemPropertyConfigurationOverrideProvider.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/SystemPropertyConfigurationOverrideProvider.java
new file mode 100644
index 000000000..54301ecc4
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/impl/override/SystemPropertyConfigurationOverrideProvider.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.impl.override;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Properties;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.caconfig.spi.ConfigurationOverrideProvider;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+/**
+ * Provides parameter override map from system properties.
+ */
+@Component(service = ConfigurationOverrideProvider.class, immediate = true)
+@Designate(ocd = SystemPropertyConfigurationOverrideProvider.Config.class)
+public final class SystemPropertyConfigurationOverrideProvider implements ConfigurationOverrideProvider {
+
+ /**
+ * Prefix for override system property
+ */
+ public static final String SYSTEM_PROPERTY_PREFIX = "sling.caconfig.override.";
+
+ @ObjectClassDefinition(name = "Apache Sling Context-Aware Configuration Override Provider: System Properties",
+ description = "Allows to override configuration property values from system environment properties.")
+ public static @interface Config {
+
+ @AttributeDefinition(name = "Enabled",
+ description = "Enable this override provider.")
+ boolean enabled() default false;
+
+ @AttributeDefinition(name = "Service Ranking",
+ description = "Priority of configuration override providers (higher = higher priority).")
+ int service_ranking() default 200;
+
+ }
+
+ private Collection overrideStrings;
+
+ @Activate
+ void activate(Config config) {
+ List overrides = new ArrayList<>();
+
+ if (config.enabled()) {
+ Properties properties = System.getProperties();
+ Enumeration keys = properties.keys();
+ while (keys.hasMoreElements()) {
+ Object keyObject = keys.nextElement();
+ if (keyObject instanceof String) {
+ String key = (String) keyObject;
+ if (StringUtils.startsWith(key, SYSTEM_PROPERTY_PREFIX)) {
+ overrides.add(StringUtils.substringAfter(key, SYSTEM_PROPERTY_PREFIX) + "=" + System.getProperty(key));
+ }
+ }
+ }
+ }
+
+ this.overrideStrings = overrides;
+ }
+
+ @Override
+ public @NotNull Collection getOverrideStrings() {
+ return overrideStrings;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationCollectionData.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationCollectionData.java
new file mode 100644
index 000000000..331d00f01
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationCollectionData.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management;
+
+import java.util.Collection;
+import java.util.Map;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Provides access to the configuration collection data and metadata for a given context path.
+ */
+@ProviderType
+public interface ConfigurationCollectionData {
+
+ /**
+ * Get configuration name.
+ * @return Configuration name
+ */
+ @NotNull String getConfigName();
+
+ /**
+ * @return Configuration collection items
+ */
+ @NotNull Collection getItems();
+
+ /**
+ * @return Path of the configuration collection resource parent path or null if it cannot be determined.
+ */
+ @Nullable String getResourcePath();
+
+ /**
+ * @return Properties for the configuration collection itself. Does not contain configuration data, but control data e.g. for enabling collection inheritance.
+ */
+ @NotNull Map getProperties();
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationData.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationData.java
new file mode 100644
index 000000000..75ff5c008
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationData.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management;
+
+import java.util.Set;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Provides access to the configuration data and metadata for a given context path.
+ */
+@ProviderType
+public interface ConfigurationData {
+
+ /**
+ * Get configuration name.
+ * @return Configuration name
+ */
+ @NotNull String getConfigName();
+
+ /**
+ * In case of configuration resource collection, this returns the collection item resource name.
+ * @return Item resource name or null if it is a singleton resource.
+ */
+ @Nullable String getCollectionItemName();
+
+ /**
+ * @return Path of the configuration resource or null if it cannot be determined.
+ */
+ @Nullable String getResourcePath();
+
+ /**
+ * List of effective property names defined in configuration metadata or values are defined for.
+ * @return Property names
+ */
+ @NotNull Set getPropertyNames();
+
+ /**
+ * Configuration values stored for the given context path. No inherited values. No default values.
+ * The properties of the resource identified by {@link #getResourcePath()} are returned.
+ * If this resources does not exist, the map is empty.
+ * @return Values
+ */
+ @NotNull ValueMap getValues();
+
+ /**
+ * Configuration values stored for the given context path merged with inherited values and default values.
+ * @return Values
+ */
+ @NotNull ValueMap getEffectiveValues();
+
+ /**
+ * Get detailed metadata information about the property value.
+ * @param propertyName Property name
+ * @return Value information. Null if neither property metadata nor an existing value exists.
+ */
+ @Nullable ValueInfo> getValueInfo(String propertyName);
+
+ /**
+ * @return true if the whole configuration is inherited.
+ */
+ boolean isInherited();
+
+ /**
+ * @return true if the whole configuration is overridden by an configuration override provider.
+ */
+ boolean isOverridden();
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationManagementSettings.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationManagementSettings.java
new file mode 100644
index 000000000..0b865cfa4
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationManagementSettings.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management;
+
+import java.util.Collection;
+import java.util.Set;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Settings for reading and writing configurations.
+ */
+@ProviderType
+public interface ConfigurationManagementSettings {
+
+ /**
+ * Detects property names that should be ignored/filtered out when reading or writing configuration data properties.
+ * @param propertyNames Existing property names to evaluate.
+ * @return Property names that should be ignored/filtered out from the given set of property names.
+ */
+ Set getIgnoredPropertyNames(Set propertyNames);
+
+ /**
+ * @return Config collection parent properties resource names.
+ */
+ Collection getConfigCollectionPropertiesResourceNames();
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationManager.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationManager.java
new file mode 100644
index 000000000..518b2c511
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationManager.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management;
+
+import java.util.SortedSet;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.caconfig.spi.ConfigurationCollectionPersistData;
+import org.apache.sling.caconfig.spi.ConfigurationPersistData;
+import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Manages reading and writing configurations e.g. for Editor GUIs.
+ * It manages only configurations handled by {@link org.apache.sling.caconfig.ConfigurationResolver},
+ * no low-level configuration resources managed by {@link org.apache.sling.caconfig.resource.ConfigurationResourceResolver}.
+ */
+@ProviderType
+public interface ConfigurationManager {
+
+ /**
+ * Get configuration data for the given context resource and configuration name.
+ * @param resource Context resource
+ * @param configName Configuration name
+ * @return Configuration data. Is null when no configuration resource found and no configuration metadata exists.
+ */
+ @Nullable ConfigurationData getConfiguration(@NotNull Resource resource, @NotNull String configName);
+
+ /**
+ * Get configuration data collection for the given context resource and configuration name.
+ * @param resource Context resource
+ * @param configName Configuration name
+ * @return Configuration data collection. Is empty when no configuration resources found.
+ */
+ @NotNull ConfigurationCollectionData getConfigurationCollection(@NotNull Resource resource, @NotNull String configName);
+
+ /**
+ * Write configuration data to repository using the inner-most context path as reference.
+ * @param resource Context resource
+ * @param configName Configuration name
+ * @param data Configuration data to be stored. All existing properties are erased and replaced with the new ones.
+ */
+ void persistConfiguration(@NotNull Resource resource, @NotNull String configName,
+ @NotNull ConfigurationPersistData data);
+
+ /**
+ * Write configuration data collection using the inner-most context path as reference.
+ * @param resource Context resource
+ * @param configName Configuration name
+ * @param data Configuration collection data to be stored. All existing collection entries on this context path level are erased and replaced with the new ones.
+ */
+ void persistConfigurationCollection(@NotNull Resource resource, @NotNull String configName,
+ @NotNull ConfigurationCollectionPersistData data);
+
+ /**
+ * Creates a new empty configuration data item for a configuration data collection for the given configuration name.
+ * @param resource Context resource
+ * @param configName Configuration name
+ * @return Configuration data. Is null when no configuration metadata exists.
+ */
+ @Nullable ConfigurationData newCollectionItem(@NotNull Resource resource, @NotNull String configName);
+
+ /**
+ * Delete configuration or configuration collection data from repository using the inner-most context path as reference.
+ * @param resource Context resource
+ * @param configName Configuration name
+ */
+ void deleteConfiguration(@NotNull Resource resource, @NotNull String configName);
+
+ /**
+ * Get all configuration names.
+ * The results of all configuration metadata provider implementations are merged.
+ * @return Configuration names
+ */
+ @NotNull SortedSet getConfigurationNames();
+
+ /**
+ * Get configuration metadata from any configuration metadata provider.
+ * @param configName Configuration name
+ * @return Configuration metadata or null if none exists for the given name.
+ */
+ @Nullable ConfigurationMetadata getConfigurationMetadata(@NotNull String configName);
+
+ /**
+ * Rewrite given resource path or configuration name according to current persistence strategies.
+ * @param configResourcePath Resource path or config name
+ * @return Rewritten resource path or config name
+ * @deprecated Please use {@link org.apache.sling.caconfig.management.multiplexer.ConfigurationPersistenceStrategyMultiplexer} instead.
+ */
+ @Deprecated
+ @Nullable String getPersistenceResourcePath(@NotNull String configResourcePath);
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationResourceResolverConfig.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationResourceResolverConfig.java
new file mode 100644
index 000000000..fea212499
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ConfigurationResourceResolverConfig.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management;
+
+import java.util.Collection;
+
+import org.jetbrains.annotations.NotNull;
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Service for accessing the ConfigurationResourceResolver configuration from other services.
+ */
+@ProviderType
+public interface ConfigurationResourceResolverConfig {
+
+ /**
+ * All bucket names starting with sling:configs and the alternative ones.
+ * @return Bucket names
+ */
+ @NotNull Collection configBucketNames();
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ContextPathStrategyMultiplexer.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ContextPathStrategyMultiplexer.java
new file mode 100644
index 000000000..c1fe8d018
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ContextPathStrategyMultiplexer.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management;
+
+import org.apache.sling.caconfig.resource.spi.ContextPathStrategy;
+
+/**
+ * Detects all {@link ContextPathStrategy} implementations in the container
+ * and consolidates their result based on service ranking.
+ * @deprecated Please use {@link org.apache.sling.caconfig.management.multiplexer.ContextPathStrategyMultiplexer} instead.
+ */
+@Deprecated
+public interface ContextPathStrategyMultiplexer extends ContextPathStrategy {
+
+ // inherits all methods from {@link ContextPathStrategy}
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ValueInfo.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ValueInfo.java
new file mode 100644
index 000000000..8bf630711
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/ValueInfo.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management;
+
+import org.apache.sling.caconfig.spi.metadata.PropertyMetadata;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Provides detailed information about a given configuration value.
+ * @param Property type
+ */
+@ProviderType
+public interface ValueInfo {
+
+ /**
+ * Property name.
+ * @return Property name.
+ */
+ @NotNull String getName();
+
+ /**
+ * Property metadata.
+ * @return Property metadata. Null if no metadata exists.
+ */
+ @Nullable PropertyMetadata getPropertyMetadata();
+
+ /**
+ * Get value stored for the current context path. No inherited value. No default value.
+ * @return Value
+ */
+ @Nullable T getValue();
+
+ /**
+ * Get value storedf or the current context path, or inherited from upper levels, or the default value.
+ * @return Value
+ */
+ @Nullable T getEffectiveValue();
+
+ /**
+ * Get the path of the configuration resource the value is stored in.
+ * @return Resource path or null if no resource associated.
+ */
+ @Nullable String getConfigSourcePath();
+
+ /**
+ * @return true if no value is defined but a default value is returned.
+ */
+ boolean isDefault();
+
+ /**
+ * @return true if the value is not defined for the current context path but inherited from upper levels.
+ */
+ boolean isInherited();
+
+ /**
+ * @return true if the value is overridden by an configuration override provider.
+ */
+ boolean isOverridden();
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationCollectionDataImpl.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationCollectionDataImpl.java
new file mode 100644
index 000000000..ce1f5f5f3
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationCollectionDataImpl.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management.impl;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.caconfig.management.ConfigurationCollectionData;
+import org.apache.sling.caconfig.management.ConfigurationData;
+import org.apache.sling.caconfig.management.ConfigurationManagementSettings;
+import org.apache.sling.caconfig.resource.impl.def.ConfigurationResourceNameConstants;
+import org.jetbrains.annotations.NotNull;
+
+final class ConfigurationCollectionDataImpl implements ConfigurationCollectionData {
+
+ private final String configName;
+ private final Collection items;
+ private final String resourcePath;
+ private final Map properties;
+ private final ConfigurationManagementSettings configurationManagementSettings;
+ private Map filteredPropertiesCache;
+
+ public ConfigurationCollectionDataImpl(String configName, Collection items,
+ String resourcePath, Map properties,
+ ConfigurationManagementSettings configurationManagementSettings) {
+ this.configName = configName;
+ this.items = items;
+ this.resourcePath = resourcePath;
+ this.properties = properties;
+ this.configurationManagementSettings = configurationManagementSettings;
+ }
+
+ @Override
+ public @NotNull String getConfigName() {
+ return configName;
+ }
+
+ @Override
+ public @NotNull Collection getItems() {
+ return items;
+ }
+
+ @Override
+ public String getResourcePath() {
+ return resourcePath;
+ }
+
+ @Override
+ public @NotNull Map getProperties() {
+ if (filteredPropertiesCache == null) {
+ filteredPropertiesCache = new HashMap<>();
+ if (properties != null) {
+ filteredPropertiesCache.putAll(properties);
+ PropertiesFilterUtil.removeIgnoredProperties(filteredPropertiesCache, configurationManagementSettings);
+ }
+ else {
+ filteredPropertiesCache.put(ConfigurationResourceNameConstants.PROPERTY_CONFIG_COLLECTION_INHERIT, true);
+ }
+ }
+ return filteredPropertiesCache;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationDataImpl.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationDataImpl.java
new file mode 100644
index 000000000..6d43dcf00
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationDataImpl.java
@@ -0,0 +1,281 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management.impl;
+
+import java.lang.reflect.Array;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.commons.collections4.IteratorUtils;
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.caconfig.management.ConfigurationData;
+import org.apache.sling.caconfig.management.ConfigurationManagementSettings;
+import org.apache.sling.caconfig.management.ConfigurationManager;
+import org.apache.sling.caconfig.management.ValueInfo;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationOverrideMultiplexer;
+import org.apache.sling.caconfig.spi.ConfigurationPersistenceStrategy2;
+import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata;
+import org.apache.sling.caconfig.spi.metadata.PropertyMetadata;
+import org.jetbrains.annotations.NotNull;
+
+final class ConfigurationDataImpl implements ConfigurationData {
+
+ private final ConfigurationMetadata configMetadata;
+ private final Resource resolvedConfigurationResource;
+ private final Resource writebackConfigurationResource;
+ private final List configurationResourceInheritanceChain;
+ private final Resource contextResource;
+ private final String configName;
+ private final ConfigurationManager configurationManager;
+ private final ConfigurationManagementSettings configurationManagementSettings;
+ private final ConfigurationOverrideMultiplexer configurationOverrideMultiplexer;
+ private final ConfigurationPersistenceStrategy2 configurationPersistenceStrategy;
+ private final boolean configResourceCollection;
+ private final String collectionItemName;
+ private final boolean isAllOverridden;
+
+ private Set propertyNamesCache;
+ private ValueMap valuesCache;
+ private ValueMap effectiveValuesCache;
+
+ public ConfigurationDataImpl(ConfigurationMetadata configMetadata,
+ Resource resolvedConfigurationResource, Resource writebackConfigurationResource,
+ Iterator configurationResourceInheritanceChain,
+ Resource contextResource, String configName,
+ ConfigurationManager configurationManager,
+ ConfigurationManagementSettings configurationManagementSettings,
+ ConfigurationOverrideMultiplexer configurationOverrideMultiplexer,
+ ConfigurationPersistenceStrategy2 configurationPersistenceStrategy,
+ boolean configResourceCollection, String collectionItemName) {
+ this.configMetadata = configMetadata;
+ this.resolvedConfigurationResource = resolvedConfigurationResource;
+ this.writebackConfigurationResource = writebackConfigurationResource;
+ this.configurationResourceInheritanceChain = configurationResourceInheritanceChain != null
+ ? IteratorUtils.toList(configurationResourceInheritanceChain) : null;
+ this.contextResource = contextResource;
+ this.configName = configName;
+ this.configurationManager = configurationManager;
+ this.configurationManagementSettings = configurationManagementSettings;
+ this.configurationOverrideMultiplexer = configurationOverrideMultiplexer;
+ this.configurationPersistenceStrategy = configurationPersistenceStrategy;
+ this.configResourceCollection = configResourceCollection;
+ this.collectionItemName = collectionItemName;
+ this.isAllOverridden = contextResource != null ? configurationOverrideMultiplexer.isAllOverridden(contextResource.getPath(), configName) : false;
+ }
+
+ public ConfigurationDataImpl(ConfigurationMetadata configMetadata,
+ Resource contextResource, String configName,
+ ConfigurationManager configurationManager,
+ ConfigurationManagementSettings configurationManagementSettings,
+ ConfigurationOverrideMultiplexer configurationOverrideMultiplexer,
+ ConfigurationPersistenceStrategy2 configurationPersistenceStrategy,
+ boolean configResourceCollection) {
+ this(configMetadata, null, null, null,
+ contextResource, configName,
+ configurationManager,
+ configurationManagementSettings,
+ configurationOverrideMultiplexer,
+ configurationPersistenceStrategy,
+ configResourceCollection, null);
+ }
+
+ @Override
+ public @NotNull String getConfigName() {
+ return configName;
+ }
+
+ @Override
+ public String getCollectionItemName() {
+ return collectionItemName;
+ }
+
+ @Override
+ public String getResourcePath() {
+ if (writebackConfigurationResource != null) {
+ return writebackConfigurationResource.getPath();
+ }
+ if (resolvedConfigurationResource != null) {
+ return resolvedConfigurationResource.getPath();
+ }
+ return null;
+ }
+
+ @Override
+ public @NotNull Set getPropertyNames() {
+ if (propertyNamesCache == null) {
+ propertyNamesCache = new LinkedHashSet<>();
+ if (configMetadata != null) {
+ propertyNamesCache.addAll(configMetadata.getPropertyMetadata().keySet());
+ }
+ if (resolvedConfigurationResource != null) {
+ propertyNamesCache.addAll(new TreeSet<>(ResourceUtil.getValueMap(resolvedConfigurationResource).keySet()));
+ }
+ PropertiesFilterUtil.removeIgnoredProperties(propertyNamesCache, configurationManagementSettings);
+ }
+ return propertyNamesCache;
+ }
+
+ @Override
+ public @NotNull ValueMap getValues() {
+ if (valuesCache == null) {
+ Map props = new HashMap<>();
+ if (writebackConfigurationResource != null) {
+ props.putAll(ResourceUtil.getValueMap(writebackConfigurationResource));
+ }
+ PropertiesFilterUtil.removeIgnoredProperties(props, configurationManagementSettings);
+ resolveNestedConfigs(props);
+ valuesCache = new ValueMapDecorator(props);
+ }
+ return valuesCache;
+ }
+
+ @Override
+ public @NotNull ValueMap getEffectiveValues() {
+ if (effectiveValuesCache == null) {
+ Map props = new HashMap<>();
+ if (configMetadata != null) {
+ for (PropertyMetadata> propertyMetadata : configMetadata.getPropertyMetadata().values()) {
+ if (propertyMetadata.getDefaultValue() != null) {
+ props.put(propertyMetadata.getName(), propertyMetadata.getDefaultValue());
+ }
+ }
+ }
+ if (resolvedConfigurationResource != null) {
+ props.putAll(ResourceUtil.getValueMap(resolvedConfigurationResource));
+ }
+ PropertiesFilterUtil.removeIgnoredProperties(props, configurationManagementSettings);
+ resolveNestedConfigs(props);
+ effectiveValuesCache = new ValueMapDecorator(props);
+ }
+ return effectiveValuesCache;
+ }
+
+ private void resolveNestedConfigs(Map props) {
+ if (configMetadata == null) {
+ return;
+ }
+ for (PropertyMetadata> propertyMetadata : configMetadata.getPropertyMetadata().values()) {
+ if (propertyMetadata.isNestedConfiguration()) {
+ ConfigurationMetadata nestedConfigMetadata = propertyMetadata.getConfigurationMetadata();
+ String relatedConfigPath = resolvedConfigurationResource != null ? resolvedConfigurationResource.getPath() : null;
+ String nestedConfigName;
+ if (configResourceCollection) {
+ String collectionItemName = StringUtils.defaultString(getCollectionItemName(), "newItem");
+ nestedConfigName = configurationPersistenceStrategy.getCollectionParentConfigName(configName, relatedConfigPath)
+ + "/" + configurationPersistenceStrategy.getCollectionItemConfigName(collectionItemName, relatedConfigPath)
+ + "/" + nestedConfigMetadata.getName();
+ }
+ else {
+ nestedConfigName = configurationPersistenceStrategy.getConfigName(configName, relatedConfigPath) + "/" + nestedConfigMetadata.getName();
+ }
+ if (propertyMetadata.getType().equals(ConfigurationMetadata.class)) {
+ ConfigurationData configData = configurationManager.getConfiguration(contextResource, nestedConfigName);
+ props.put(propertyMetadata.getName(), configData);
+ }
+ else if (propertyMetadata.getType().equals(ConfigurationMetadata[].class)) {
+ Collection configDatas = configurationManager.getConfigurationCollection(contextResource, nestedConfigName).getItems();
+ props.put(propertyMetadata.getName(), configDatas.toArray(new ConfigurationData[configDatas.size()]));
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public ValueInfo> getValueInfo(String propertyName) {
+ PropertyMetadata> propertyMetadata = getPropertyMetadata(propertyName);
+ Object value;
+ Object effectiveValue;
+ if (propertyMetadata != null) {
+ Class> type = propertyMetadata.getType();
+ if (type == ConfigurationMetadata.class) {
+ type = ConfigurationData.class;
+ }
+ else if (type == ConfigurationMetadata[].class) {
+ type = ConfigurationData[].class;
+ }
+ else {
+ type = primitiveToWrapper(type);
+ }
+ value = getValues().get(propertyName, type);
+ effectiveValue = getEffectiveValues().get(propertyName, type);
+ }
+ else {
+ value = getValues().get(propertyName);
+ effectiveValue = getEffectiveValues().get(propertyName);
+ }
+ return new ValueInfoImpl(propertyName, value, effectiveValue, propertyMetadata,
+ resolvedConfigurationResource,
+ writebackConfigurationResource,
+ configurationResourceInheritanceChain,
+ contextResource,
+ configName,
+ configurationOverrideMultiplexer,
+ isAllOverridden);
+ }
+
+ private Class> primitiveToWrapper(Class> type) {
+ if (type.isArray()) {
+ return Array.newInstance(ClassUtils.primitiveToWrapper(type.getComponentType()), 0).getClass();
+ }
+ else {
+ return ClassUtils.primitiveToWrapper(type);
+ }
+ }
+
+ private PropertyMetadata> getPropertyMetadata(String propertyName) {
+ if (configMetadata == null) {
+ return null;
+ }
+ else {
+ return configMetadata.getPropertyMetadata().get(propertyName);
+ }
+ }
+
+ @Override
+ public boolean isInherited() {
+ // detect if the whole config or config item was inherited
+ if (resolvedConfigurationResource != null && resolvedConfigurationResource.getPath() != null) {
+ if (writebackConfigurationResource == null) {
+ return true;
+ }
+ else {
+ return !StringUtils.equals(writebackConfigurationResource.getPath(), resolvedConfigurationResource.getPath());
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isOverridden() {
+ return isAllOverridden;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationManagementSettingsImpl.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationManagementSettingsImpl.java
new file mode 100644
index 000000000..3124d85cd
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationManagementSettingsImpl.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management.impl;
+
+import static org.apache.sling.api.resource.ResourceResolver.PROPERTY_RESOURCE_TYPE;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+
+import org.apache.sling.caconfig.management.ConfigurationManagementSettings;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.Designate;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(service = ConfigurationManagementSettings.class)
+@Designate(ocd=ConfigurationManagementSettingsImpl.Config.class)
+public class ConfigurationManagementSettingsImpl implements ConfigurationManagementSettings {
+
+ @ObjectClassDefinition(name="Apache Sling Context-Aware Configuration Management Settings",
+ description="Management settings for reading and writing configurations.")
+ static @interface Config {
+
+ @AttributeDefinition(name="Ignore Property Regex",
+ description = "List of regular expressions with property names that should be ignored when reading or writing configuration data properties.")
+ String[] ignorePropertyNameRegex() default {
+ "^jcr:.+$",
+ "^" + PROPERTY_RESOURCE_TYPE + "$"
+ };
+
+ @AttributeDefinition(name="Config collection parent properties resource names",
+ description = "Names of resource to try to look up configuration collection properties in. If list is empty only the collection parent resource is checked." +
+ " If the list is not empty than only those listed resources are used for look up. If you want to include the collection parent resource you can use a dot for the value.")
+ String[] configCollectionPropertiesResourceNames();
+
+ }
+
+ private static final Logger log = LoggerFactory.getLogger(ConfigurationManagementSettingsImpl.class);
+
+ private Pattern[] ignorePropertyNameRegex;
+ private Collection configCollectionPropertiesResourceNames;
+
+
+ @Activate
+ private void activate(Config config) {
+ List patterns = new ArrayList<>();
+ for (String patternString : config.ignorePropertyNameRegex()) {
+ try {
+ patterns.add(Pattern.compile(patternString));
+ }
+ catch (PatternSyntaxException ex) {
+ log.warn("Ignoring invalid regex pattern: " + patternString, ex);
+ }
+ }
+
+ this.ignorePropertyNameRegex = patterns.toArray(new Pattern[patterns.size()]);
+
+ String[] configCollectionPropertiesResourceNames = config.configCollectionPropertiesResourceNames();
+ if (configCollectionPropertiesResourceNames == null || configCollectionPropertiesResourceNames.length == 0) {
+ configCollectionPropertiesResourceNames = new String[] { "." };
+ }
+ this.configCollectionPropertiesResourceNames = Collections.unmodifiableList(Arrays.asList(configCollectionPropertiesResourceNames));
+ }
+
+ @Override
+ public Set getIgnoredPropertyNames(Set propertyNames) {
+ Set ignoredPropertyNames = new HashSet<>();
+ for (String propertyName : propertyNames) {
+ for (Pattern ignorePattern : ignorePropertyNameRegex) {
+ if (ignorePattern.matcher(propertyName).matches()) {
+ ignoredPropertyNames.add(propertyName);
+ break;
+ }
+ }
+ }
+ return ignoredPropertyNames;
+ }
+
+ @Override
+ public Collection getConfigCollectionPropertiesResourceNames() {
+ return configCollectionPropertiesResourceNames;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationManagerImpl.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationManagerImpl.java
new file mode 100644
index 000000000..fc46b7c5b
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationManagerImpl.java
@@ -0,0 +1,425 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management.impl;
+
+import static org.apache.sling.caconfig.impl.ConfigurationNameConstants.CONFIGS_BUCKET_NAME;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+
+import org.apache.commons.collections4.IteratorUtils;
+import org.apache.commons.collections4.ResettableIterator;
+import org.apache.commons.collections4.Transformer;
+import org.apache.commons.collections4.iterators.ListIteratorWrapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.caconfig.management.ConfigurationCollectionData;
+import org.apache.sling.caconfig.management.ConfigurationData;
+import org.apache.sling.caconfig.management.ConfigurationManagementSettings;
+import org.apache.sling.caconfig.management.ConfigurationManager;
+import org.apache.sling.caconfig.management.ConfigurationResourceResolverConfig;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationInheritanceStrategyMultiplexer;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationMetadataProviderMultiplexer;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationOverrideMultiplexer;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationPersistenceStrategyMultiplexer;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationResourceResolvingStrategyMultiplexer;
+import org.apache.sling.caconfig.resource.impl.util.ConfigNameUtil;
+import org.apache.sling.caconfig.resource.impl.util.MapUtil;
+import org.apache.sling.caconfig.spi.ConfigurationCollectionPersistData;
+import org.apache.sling.caconfig.spi.ConfigurationPersistData;
+import org.apache.sling.caconfig.spi.ConfigurationPersistenceException;
+import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata;
+import org.apache.sling.caconfig.spi.metadata.PropertyMetadata;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(service = ConfigurationManager.class)
+public class ConfigurationManagerImpl implements ConfigurationManager {
+
+ @Reference
+ private ConfigurationResourceResolvingStrategyMultiplexer configurationResourceResolvingStrategy;
+ @Reference
+ private ConfigurationMetadataProviderMultiplexer configurationMetadataProvider;
+ @Reference
+ private ConfigurationPersistenceStrategyMultiplexer configurationPersistenceStrategy;
+ @Reference
+ private ConfigurationInheritanceStrategyMultiplexer configurationInheritanceStrategy;
+ @Reference
+ private ConfigurationOverrideMultiplexer configurationOverrideMultiplexer;
+ @Reference
+ private ConfigurationResourceResolverConfig configurationResourceResolverConfig;
+ @Reference
+ private ConfigurationManagementSettings configurationManagementSettings;
+
+ private static final Logger log = LoggerFactory.getLogger(ConfigurationManagerImpl.class);
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public ConfigurationData getConfiguration(@NotNull Resource resource, @NotNull String configName) {
+ ConfigNameUtil.ensureValidConfigName(configName);
+ if (log.isDebugEnabled()) {
+ log.debug("Get configuration for context path {}, name '{}'", resource.getPath(), configName);
+ }
+ ConfigurationMetadata configMetadata = getConfigurationMetadata(configName);
+ Resource configResource = null;
+
+ Iterator configResourceInheritanceChain = configurationResourceResolvingStrategy
+ .getResourceInheritanceChain(resource, configurationResourceResolverConfig.configBucketNames(), configName);
+
+ if (configResourceInheritanceChain != null) {
+ ResettableIterator resettableConfigResourceInheritanceChain = new ListIteratorWrapper(configResourceInheritanceChain);
+ configResource = applyPersistenceAndInheritance(resource.getPath(), configName, resettableConfigResourceInheritanceChain, false, resource.getResourceResolver());
+ if (configResource != null) {
+ // get writeback resource for "reverse inheritance detection"
+ Resource writebackConfigResource = null;
+
+ String writebackConfigResourcePath = null;
+ for (String configBucketName : configurationResourceResolverConfig.configBucketNames()) {
+ writebackConfigResourcePath = configurationResourceResolvingStrategy.getResourcePath(resource, configBucketName, configName);
+ if (writebackConfigResourcePath != null) {
+ writebackConfigResource = resource.getResourceResolver().getResource(writebackConfigResourcePath);
+ if (writebackConfigResource != null) {
+ writebackConfigResource = configurationPersistenceStrategy.getResource(writebackConfigResource);
+ break;
+ }
+ }
+ }
+
+ if (log.isTraceEnabled()) {
+ log.trace("+ Found config resource for context path " + resource.getPath() + ": " + configResource.getPath() + " "
+ + MapUtil.traceOutput(configResource.getValueMap()) + ", "
+ + "writeback config resource: " + writebackConfigResourcePath);
+ }
+ resettableConfigResourceInheritanceChain.reset();
+ return new ConfigurationDataImpl(configMetadata, configResource, writebackConfigResource,
+ applyPersistence(resettableConfigResourceInheritanceChain, false),
+ resource, configName, this, configurationManagementSettings,
+ configurationOverrideMultiplexer, configurationPersistenceStrategy, false, null);
+ }
+ }
+ if (configMetadata != null) {
+ // if no config resource found still check for overrides
+ configResource = configurationOverrideMultiplexer.overrideProperties(resource.getPath(), configName, (Resource)null, resource.getResourceResolver());
+ if (configResource != null) {
+ return new ConfigurationDataImpl(configMetadata, configResource, null, null,
+ resource, configName, this, configurationManagementSettings,
+ configurationOverrideMultiplexer, configurationPersistenceStrategy, false, null);
+ }
+
+ // if no config resource found but config metadata exist return empty config data with default values
+ return new ConfigurationDataImpl(configMetadata,
+ resource, configName, this, configurationManagementSettings,
+ configurationOverrideMultiplexer, configurationPersistenceStrategy, false);
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public@NotNull ConfigurationCollectionData getConfigurationCollection(@NotNull Resource resource, @NotNull String configName) {
+ ConfigNameUtil.ensureValidConfigName(configName);
+ if (log.isDebugEnabled()) {
+ log.debug("Get configuration collection for context path {}, name '{}'", resource.getPath(), configName);
+ }
+ ConfigurationMetadata configMetadata = getConfigurationMetadata(configName);
+ List configData = new ArrayList<>();
+
+ // get all possible colection parent config names
+ Collection collectionParentConfigNames = configurationPersistenceStrategy.getAllCollectionParentConfigNames(configName);
+
+ // get configuration resource items
+ List> configResourceInheritanceChains = new ArrayList<>();
+ for (String collectionParentConfigName : collectionParentConfigNames) {
+ Collection> result = configurationResourceResolvingStrategy
+ .getResourceCollectionInheritanceChain(resource, configurationResourceResolverConfig.configBucketNames(), collectionParentConfigName);
+ if (result != null) {
+ configResourceInheritanceChains.addAll(result);
+ }
+ }
+
+ String writebackConfigResourceCollectionParentPath = null;
+ for (Iterator configResourceInheritanceChain : configResourceInheritanceChains) {
+ ResettableIterator resettableConfigResourceInheritanceChain = new ListIteratorWrapper(configResourceInheritanceChain);
+ Resource configResource = applyPersistenceAndInheritance(resource.getPath(), configName, resettableConfigResourceInheritanceChain, true, resource.getResourceResolver());
+ resettableConfigResourceInheritanceChain.reset();
+ Resource untransformedConfigResource = (Resource)resettableConfigResourceInheritanceChain.next();
+ if (configResource != null) {
+ // get writeback resource for "reverse inheritance detection"
+ Resource writebackConfigResource = null;
+
+ String writebackConfigResourcePath = null;
+ for (String configBucketName : configurationResourceResolverConfig.configBucketNames()) {
+ writebackConfigResourceCollectionParentPath = configurationResourceResolvingStrategy.getResourceCollectionParentPath(resource, configBucketName, configName);
+ if (writebackConfigResourceCollectionParentPath != null) {
+ writebackConfigResourceCollectionParentPath = configurationPersistenceStrategy.getCollectionParentResourcePath(writebackConfigResourceCollectionParentPath);
+ writebackConfigResourcePath = writebackConfigResourceCollectionParentPath + "/" + untransformedConfigResource.getName();
+ writebackConfigResource = configResource.getResourceResolver().getResource(writebackConfigResourcePath);
+ if (writebackConfigResource != null) {
+ writebackConfigResource = configurationPersistenceStrategy.getCollectionItemResource(writebackConfigResource);
+ break;
+ }
+ }
+ }
+
+ if (log.isTraceEnabled()) {
+ log.trace("+ Found config resource for context path " + resource.getPath() + ": " + configResource.getPath() + " "
+ + MapUtil.traceOutput(configResource.getValueMap()) + ", "
+ + "writeback config resource: " + writebackConfigResourcePath);
+ }
+ resettableConfigResourceInheritanceChain.reset();
+ configData.add(new ConfigurationDataImpl(configMetadata, configResource, writebackConfigResource,
+ applyPersistence(resettableConfigResourceInheritanceChain, true),
+ resource, configName, this, configurationManagementSettings,
+ configurationOverrideMultiplexer, configurationPersistenceStrategy,
+ true, untransformedConfigResource.getName()));
+ }
+ }
+ // fallback for writeback path detection when no configuration resources does exist yet
+ if (writebackConfigResourceCollectionParentPath == null) {
+ for (String configBucketName : configurationResourceResolverConfig.configBucketNames()) {
+ writebackConfigResourceCollectionParentPath = configurationResourceResolvingStrategy.getResourceCollectionParentPath(resource, configBucketName, configName);
+ if (writebackConfigResourceCollectionParentPath != null) {
+ break;
+ }
+ }
+ }
+
+ // get properties of parent resource of the current level
+ Map resourceCollectionParentProps = null;
+ if (writebackConfigResourceCollectionParentPath != null) {
+ Resource writebackConfigResourceCollectionParent = resource.getResourceResolver().getResource(writebackConfigResourceCollectionParentPath);
+ if (writebackConfigResourceCollectionParent != null) {
+ for (String resourceName : configurationManagementSettings.getConfigCollectionPropertiesResourceNames()) {
+ Resource propsResource = writebackConfigResourceCollectionParent.getChild(resourceName);
+ if (propsResource != null) {
+ resourceCollectionParentProps = propsResource.getValueMap();
+ break;
+ }
+ }
+ }
+ }
+
+ return new ConfigurationCollectionDataImpl(
+ configName,
+ configData,
+ writebackConfigResourceCollectionParentPath,
+ resourceCollectionParentProps,
+ configurationManagementSettings
+ );
+ }
+
+ @SuppressWarnings("unchecked")
+ private Iterator applyPersistence(final Iterator configResourceInheritanceChain, final boolean isCollection) {
+ if (configResourceInheritanceChain == null) {
+ return null;
+ }
+ return IteratorUtils.transformedIterator(configResourceInheritanceChain,
+ new Transformer() {
+ @Override
+ public Object transform(Object input) {
+ if (isCollection) {
+ return configurationPersistenceStrategy.getCollectionItemResource((Resource)input);
+ }
+ else {
+ return configurationPersistenceStrategy.getResource((Resource)input);
+ }
+ }
+ });
+ }
+
+ private Resource applyPersistenceAndInheritance(String contextPath, String configName, Iterator configResourceInheritanceChain,
+ boolean isCollection, ResourceResolver resourceResolver) {
+ if (configResourceInheritanceChain == null) {
+ return null;
+ }
+
+ // apply configuration persistence transformation
+ Iterator transformedConfigResources = applyPersistence(configResourceInheritanceChain, isCollection);
+
+ // apply resource inheritance
+ Resource configResource = configurationInheritanceStrategy.getResource(transformedConfigResources);
+
+ // apply overrides
+ return configurationOverrideMultiplexer.overrideProperties(contextPath, configName, configResource, resourceResolver);
+ }
+
+ @Override
+ public void persistConfiguration(@NotNull Resource resource, @NotNull String configName, @NotNull ConfigurationPersistData data) {
+ ConfigNameUtil.ensureValidConfigName(configName);
+ String configResourcePath = configurationResourceResolvingStrategy.getResourcePath(resource, CONFIGS_BUCKET_NAME, configName);
+ if (configResourcePath == null) {
+ throw new ConfigurationPersistenceException("Unable to persist configuration: Configuration resolving strategy returned no path.");
+ }
+ if (log.isDebugEnabled()) {
+ log.debug("Persist configuration for context path {}, name '{}' to {}", resource.getPath(), configName, configResourcePath);
+ }
+ if (!configurationPersistenceStrategy.persistConfiguration(resource.getResourceResolver(), configResourcePath, data)) {
+ throw new ConfigurationPersistenceException("Unable to persist configuration: No persistence strategy found.");
+ }
+ }
+
+ @Override
+ public void persistConfigurationCollection(@NotNull Resource resource, @NotNull String configName, @NotNull ConfigurationCollectionPersistData data) {
+ ConfigNameUtil.ensureValidConfigName(configName);
+ String configResourceParentPath = configurationResourceResolvingStrategy.getResourceCollectionParentPath(resource, CONFIGS_BUCKET_NAME, configName);
+ if (configResourceParentPath == null) {
+ throw new ConfigurationPersistenceException("Unable to persist configuration collection: Configuration resolving strategy returned no parent path.");
+ }
+ if (log.isDebugEnabled()) {
+ log.debug("Persist configuration collection for context path {}, name '{}' to {}", resource.getPath(), configName, configResourceParentPath);
+ }
+ if (!configurationPersistenceStrategy.persistConfigurationCollection(resource.getResourceResolver(), configResourceParentPath, data)) {
+ throw new ConfigurationPersistenceException("Unable to persist configuration: No persistence strategy found.");
+ }
+ }
+
+ @Override
+ public ConfigurationData newCollectionItem(@NotNull Resource resource, @NotNull String configName) {
+ ConfigNameUtil.ensureValidConfigName(configName);
+ ConfigurationMetadata configMetadata = getConfigurationMetadata(configName);
+ if (configMetadata != null) {
+ return new ConfigurationDataImpl(configMetadata,
+ resource, configName, this, configurationManagementSettings,
+ configurationOverrideMultiplexer, configurationPersistenceStrategy, true);
+ }
+ return null;
+ }
+
+ @Override
+ public void deleteConfiguration(@NotNull Resource resource, @NotNull String configName) {
+ ConfigNameUtil.ensureValidConfigName(configName);
+
+ // try to delete from all config bucket names
+ boolean foundAnyPath = false;
+ for (String configBucketName : configurationResourceResolverConfig.configBucketNames()) {
+ String configResourcePath = configurationResourceResolvingStrategy.getResourcePath(resource, configBucketName, configName);
+ if (configResourcePath != null) {
+ foundAnyPath = true;
+ if (log.isDebugEnabled()) {
+ log.debug("Delete configuration for context path {}, name '{}' from {}", resource.getPath(), configName, configResourcePath);
+ }
+ if (!configurationPersistenceStrategy.deleteConfiguration(resource.getResourceResolver(), configResourcePath)) {
+ throw new ConfigurationPersistenceException("Unable to delete configuration: No persistence strategy found.");
+ }
+ }
+ }
+ if (!foundAnyPath) {
+ throw new ConfigurationPersistenceException("Unable to delete configuration: Configuration resolving strategy returned no path.");
+ }
+ }
+
+ @Override
+ public @NotNull SortedSet getConfigurationNames() {
+ return configurationMetadataProvider.getConfigurationNames();
+ }
+
+ @Override
+ public ConfigurationMetadata getConfigurationMetadata(@NotNull String configName) {
+ ConfigNameUtil.ensureValidConfigName(configName);
+ ConfigurationMetadata metadata = configurationMetadataProvider.getConfigurationMetadata(configName);
+ if (metadata != null) {
+ log.trace("+ Configuration metadata found for: {}", configName);
+ return metadata;
+ }
+
+ // if no metadata found with direct match try to resolve nested configuration metadata references
+ for (String partialConfigName : ConfigNameUtil.getAllPartialConfigNameVariations(configName)) {
+ ConfigurationMetadata partialConfigMetadata = getConfigurationMetadata(partialConfigName);
+ if (partialConfigMetadata != null) {
+ ConfigurationMetadata nestedConfigMetadata = getNestedConfigurationMetadata(partialConfigMetadata, configName, partialConfigName);
+ if (nestedConfigMetadata != null) {
+ log.trace("+ Nested configuration metadata found for: {}", configName);
+ return nestedConfigMetadata;
+ }
+ }
+ }
+
+ log.trace("- No configuration metadata found for: {}", configName);
+ return null;
+ }
+
+ private ConfigurationMetadata getNestedConfigurationMetadata(ConfigurationMetadata configMetadata, String configName, String partialConfigName) {
+ if (StringUtils.startsWith(configName, partialConfigName + "/")) {
+
+ // depending on different persistence strategies config names can be transformed differently - try all combinations here
+ Set prefixesToRemove = new LinkedHashSet<>();
+ if (configMetadata.isCollection()) {
+ String collectionItemName = StringUtils.substringBefore(StringUtils.substringAfter(configName, partialConfigName + "/"), "/");
+ for (String collectionParentConfigName : configurationPersistenceStrategy.getAllCollectionParentConfigNames(partialConfigName)) {
+ for (String collectionItemConfigName : configurationPersistenceStrategy.getAllCollectionItemConfigNames(collectionItemName)) {
+ prefixesToRemove.add(collectionParentConfigName + "/" + collectionItemConfigName + "/");
+ }
+ }
+ }
+ else {
+ for (String configNameItem : configurationPersistenceStrategy.getAllConfigNames(partialConfigName)) {
+ prefixesToRemove.add(configNameItem + "/");
+ }
+ }
+
+ for (String prefixToRemove : prefixesToRemove) {
+ String remainingConfigName = StringUtils.substringAfter(configName, prefixToRemove);
+ // try direct match
+ ConfigurationMetadata nestedConfigMetadata = getNestedConfigurationMetadataFromProperty(configMetadata, remainingConfigName);
+ if (nestedConfigMetadata != null) {
+ return nestedConfigMetadata;
+ }
+ // try to find partial match for deeper nestings
+ for (String partialRemainingConfigName : ConfigNameUtil.getAllPartialConfigNameVariations(remainingConfigName)) {
+ ConfigurationMetadata partialConfigMetadata = getNestedConfigurationMetadataFromProperty(configMetadata, partialRemainingConfigName);
+ if (partialConfigMetadata != null) {
+ nestedConfigMetadata = getNestedConfigurationMetadata(partialConfigMetadata, remainingConfigName, partialRemainingConfigName);
+ if (nestedConfigMetadata != null) {
+ return nestedConfigMetadata;
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ private ConfigurationMetadata getNestedConfigurationMetadataFromProperty(ConfigurationMetadata partialConfigMetadata, String configName) {
+ for (PropertyMetadata> propertyMetadata : partialConfigMetadata.getPropertyMetadata().values()) {
+ if (propertyMetadata.isNestedConfiguration()) {
+ ConfigurationMetadata nestedConfigMetadata = propertyMetadata.getConfigurationMetadata();
+ if (StringUtils.equals(configName, nestedConfigMetadata.getName())) {
+ return nestedConfigMetadata;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getPersistenceResourcePath(@NotNull String configResourcePath) {
+ return configurationPersistenceStrategy.getResourcePath(configResourcePath);
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationPersistenceStrategyMultiplexerImpl.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationPersistenceStrategyMultiplexerImpl.java
new file mode 100644
index 000000000..bdae9a8da
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ConfigurationPersistenceStrategyMultiplexerImpl.java
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management.impl;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationPersistenceStrategyMultiplexer;
+import org.apache.sling.caconfig.spi.ConfigurationCollectionPersistData;
+import org.apache.sling.caconfig.spi.ConfigurationPersistData;
+import org.apache.sling.caconfig.spi.ConfigurationPersistenceStrategy2;
+import org.apache.sling.commons.osgi.Order;
+import org.apache.sling.commons.osgi.RankedServices;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+
+/**
+ * Detects all {@link ConfigurationPersistenceStrategy2} implementations in the container
+ * and consolidates their result based on service ranking.
+ */
+@Component(service = ConfigurationPersistenceStrategyMultiplexer.class,
+reference={
+ @Reference(name="configurationPersistenceStrategy", service=ConfigurationPersistenceStrategy2.class,
+ bind="bindConfigurationPersistenceStrategy", unbind="unbindConfigurationPersistenceStrategy",
+ cardinality=ReferenceCardinality.MULTIPLE,
+ policy=ReferencePolicy.DYNAMIC, policyOption=ReferencePolicyOption.GREEDY)
+})
+public class ConfigurationPersistenceStrategyMultiplexerImpl implements ConfigurationPersistenceStrategyMultiplexer {
+
+ private RankedServices items = new RankedServices<>(Order.DESCENDING);
+
+ protected void bindConfigurationPersistenceStrategy(ConfigurationPersistenceStrategy2 configurationPersistenceStrategy, Map props) {
+ items.bind(configurationPersistenceStrategy, props);
+ }
+
+ protected void unbindConfigurationPersistenceStrategy(ConfigurationPersistenceStrategy2 configurationPersistenceStrategy, Map props) {
+ items.unbind(configurationPersistenceStrategy, props);
+ }
+
+ /**
+ * Transform the configuration resource by the first implementation that has an answer.
+ */
+ @Override
+ public Resource getResource(@NotNull Resource resource) {
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ Resource result = item.getResource(resource);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Resource getCollectionParentResource(@NotNull Resource resource) {
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ Resource result = item.getCollectionParentResource(resource);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public Resource getCollectionItemResource(@NotNull Resource resource) {
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ Resource result = item.getCollectionItemResource(resource);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getResourcePath(@NotNull String resourcePath) {
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ String result = item.getResourcePath(resourcePath);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getCollectionParentResourcePath(@NotNull String resourcePath) {
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ String result = item.getCollectionParentResourcePath(resourcePath);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getCollectionItemResourcePath(@NotNull String resourcePath) {
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ String result = item.getCollectionItemResourcePath(resourcePath);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getConfigName(@NotNull String configName, @Nullable String relatedConfigPath) {
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ String result = item.getConfigName(configName, relatedConfigPath);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getCollectionParentConfigName(@NotNull String configName, @Nullable String relatedConfigPath) {
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ String result = item.getCollectionParentConfigName(configName, relatedConfigPath);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getCollectionItemConfigName(@NotNull String configName, @Nullable String relatedConfigPath) {
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ String result = item.getCollectionItemConfigName(configName, relatedConfigPath);
+ if (result != null) {
+ return result;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public @NotNull Collection getAllConfigNames(@NotNull String configName) {
+ Set configNames = new LinkedHashSet<>();
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ String result = item.getConfigName(configName, null);
+ if (result != null) {
+ configNames.add(result);
+ }
+ }
+ return configNames;
+ }
+
+ @Override
+ public @NotNull Collection getAllCollectionParentConfigNames(@NotNull String configName) {
+ Set configNames = new LinkedHashSet<>();
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ String result = item.getCollectionParentConfigName(configName, null);
+ if (result != null) {
+ configNames.add(result);
+ }
+ }
+ return configNames;
+ }
+
+ @Override
+ public @NotNull Collection getAllCollectionItemConfigNames(@NotNull String configName) {
+ Set configNames = new LinkedHashSet<>();
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ String result = item.getCollectionItemConfigName(configName, null);
+ if (result != null) {
+ configNames.add(result);
+ }
+ }
+ return configNames;
+ }
+
+ /**
+ * Persist configuration data with the first implementation that accepts it.
+ */
+ @Override
+ public boolean persistConfiguration(@NotNull ResourceResolver resourceResolver, @NotNull String configResourcePath,
+ @NotNull ConfigurationPersistData data) {
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ if (item.persistConfiguration(resourceResolver, configResourcePath, data)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Persist configuration data with the first implementation that accepts it.
+ */
+ @Override
+ public boolean persistConfigurationCollection(@NotNull ResourceResolver resourceResolver, @NotNull String configResourceCollectionParentPath,
+ @NotNull ConfigurationCollectionPersistData data) {
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ if (item.persistConfigurationCollection(resourceResolver, configResourceCollectionParentPath, data)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean deleteConfiguration(@NotNull ResourceResolver resourceResolver, @NotNull String configResourcePath) {
+ for (ConfigurationPersistenceStrategy2 item : items) {
+ if (item.deleteConfiguration(resourceResolver, configResourcePath)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ContextPathStrategyMultiplexerImpl.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ContextPathStrategyMultiplexerImpl.java
new file mode 100644
index 000000000..f75c7b257
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ContextPathStrategyMultiplexerImpl.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management.impl;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.caconfig.management.multiplexer.ContextPathStrategyMultiplexer;
+import org.apache.sling.caconfig.resource.impl.util.ResourceEliminateDuplicatesIterator;
+import org.apache.sling.caconfig.resource.impl.util.ResourcePathCollatingIterator;
+import org.apache.sling.caconfig.resource.spi.ContextPathStrategy;
+import org.apache.sling.caconfig.resource.spi.ContextResource;
+import org.apache.sling.commons.osgi.Order;
+import org.apache.sling.commons.osgi.RankedServices;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicy;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+
+/**
+ * Detects all {@link ContextPathStrategy} implementations in the container
+ * and consolidates their result based on service ranking.
+ */
+@SuppressWarnings("deprecation")
+@Component(service = { ContextPathStrategyMultiplexer.class, org.apache.sling.caconfig.management.ContextPathStrategyMultiplexer.class },
+reference={
+ @Reference(name="contextPathStrategy", service=ContextPathStrategy.class,
+ bind="bindContextPathStrategy", unbind="unbindContextPathStrategy",
+ cardinality=ReferenceCardinality.MULTIPLE,
+ policy=ReferencePolicy.DYNAMIC, policyOption=ReferencePolicyOption.GREEDY)
+})
+public class ContextPathStrategyMultiplexerImpl implements ContextPathStrategyMultiplexer, org.apache.sling.caconfig.management.ContextPathStrategyMultiplexer {
+
+ private RankedServices items = new RankedServices<>(Order.DESCENDING);
+
+ protected void bindContextPathStrategy(ContextPathStrategy contextPathStrategy, Map props) {
+ items.bind(contextPathStrategy, props);
+ }
+
+ protected void unbindContextPathStrategy(ContextPathStrategy contextPathStrategy, Map props) {
+ items.unbind(contextPathStrategy, props);
+ }
+
+ /**
+ * Merges all results from the detected implementations into a single answer.
+ */
+ @Override
+ public @NotNull Iterator findContextResources(@NotNull Resource resource) {
+ List> allResults = getAllResults(resource);
+ if (allResults.isEmpty()) {
+ return Collections.emptyIterator();
+ }
+ if (allResults.size() == 1) {
+ return allResults.get(0);
+ }
+ return mergeResults(allResults);
+ }
+
+ /**
+ * Get all results from all registered context path strategies.
+ * @param resource Start resource
+ * @return List of all results
+ */
+ private List> getAllResults(Resource resource) {
+ List> results = new ArrayList<>();
+ for (ContextPathStrategy item : items) {
+ Iterator result = item.findContextResources(resource);
+ if (result.hasNext()) {
+ results.add(result);
+ }
+ }
+ return results;
+ }
+
+ /**
+ * Merges results from different context path strategy implementations.
+ * Eliminating of duplicates and sorting is done solely based on path length.
+ * The contract of the ContextPathStrategy defines that only parents or the resource itself
+ * is returned, so the assumption should be safe.
+ * @param allResults List of all results
+ * @return Merged result
+ */
+ private Iterator mergeResults(List> allResults) {
+ return new ResourceEliminateDuplicatesIterator(
+ new ResourcePathCollatingIterator(allResults)
+ );
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/PropertiesFilterUtil.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/PropertiesFilterUtil.java
new file mode 100644
index 000000000..268ee1401
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/PropertiesFilterUtil.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management.impl;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.sling.caconfig.management.ConfigurationManagementSettings;
+
+/**
+ * Filter internal properties from ConfigManager API output.
+ */
+public final class PropertiesFilterUtil {
+
+ private PropertiesFilterUtil() {
+ // static methods only
+ }
+
+ public static void removeIgnoredProperties(Set propertyNames, ConfigurationManagementSettings settings) {
+ Set ignoredProperties = settings.getIgnoredPropertyNames(propertyNames);
+ propertyNames.removeAll(ignoredProperties);
+ }
+
+ public static void removeIgnoredProperties(Map props, ConfigurationManagementSettings settings) {
+ for (String propertyName : settings.getIgnoredPropertyNames(props.keySet())) {
+ props.remove(propertyName);
+ }
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ValueInfoImpl.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ValueInfoImpl.java
new file mode 100644
index 000000000..486008021
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/ValueInfoImpl.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management.impl;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.caconfig.management.ValueInfo;
+import org.apache.sling.caconfig.management.multiplexer.ConfigurationOverrideMultiplexer;
+import org.apache.sling.caconfig.spi.metadata.PropertyMetadata;
+import org.jetbrains.annotations.NotNull;
+
+final class ValueInfoImpl implements ValueInfo {
+
+ private final String name;
+ private final T value;
+ private final T effectiveValue;
+ private final T defaultValue;
+ private final PropertyMetadata propertyMetadata;
+ private final Resource resolvedConfigurationResource;
+ private final Resource writebackConfigurationResource;
+ private final List configurationResourceInheritanceChain;
+ private final Resource contextResource;
+ private final String configName;
+ private final ConfigurationOverrideMultiplexer configurationOverrideMultiplexer;
+ private final boolean isAllOverridden;
+
+ public ValueInfoImpl(String name, T value, T effectiveValue, PropertyMetadata propertyMetadata,
+ Resource resolvedConfigurationResource, Resource writebackConfigurationResource,
+ List configurationResourceInheritanceChain,
+ Resource contextResource, String configName, ConfigurationOverrideMultiplexer configurationOverrideMultiplexer,
+ boolean isAllOverridden) {
+ this.name = name;
+ this.value = value;
+ this.effectiveValue = effectiveValue;
+ this.defaultValue = propertyMetadata != null ? propertyMetadata.getDefaultValue() : null;
+ this.propertyMetadata = propertyMetadata;
+ this.resolvedConfigurationResource = resolvedConfigurationResource;
+ this.writebackConfigurationResource = writebackConfigurationResource;
+ this.configurationResourceInheritanceChain = configurationResourceInheritanceChain;
+ this.contextResource = contextResource;
+ this.configName = configName;
+ this.configurationOverrideMultiplexer = configurationOverrideMultiplexer;
+ this.isAllOverridden = isAllOverridden;
+ }
+
+ @Override
+ public @NotNull String getName() {
+ return name;
+ }
+
+ @Override
+ public PropertyMetadata getPropertyMetadata() {
+ return propertyMetadata;
+ }
+
+ @Override
+ public T getValue() {
+ return value;
+ }
+
+ @Override
+ public T getEffectiveValue() {
+ return effectiveValue;
+ }
+
+ @Override
+ public String getConfigSourcePath() {
+ if (effectiveValue != null && resolvedConfigurationResource != null) {
+ Resource resource = getResourceFromInheritanceChain();
+ if (resource != null) {
+ return resource.getPath();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isDefault() {
+ if (defaultValue == null) {
+ return false;
+ }
+ if (resolvedConfigurationResource == null) {
+ return true;
+ }
+ else {
+ return !resolvedConfigurationResource.getValueMap().containsKey(name);
+ }
+ }
+
+ @Override
+ public boolean isInherited() {
+ if (isDefault() || effectiveValue == null) {
+ return false;
+ }
+ else if (resolvedConfigurationResource == null || resolvedConfigurationResource.getPath() == null) {
+ return false;
+ }
+ else if (writebackConfigurationResource == null) {
+ return true;
+ }
+ else if (!StringUtils.equals(resolvedConfigurationResource.getPath(), writebackConfigurationResource.getPath())) {
+ return true;
+ }
+ else {
+ Resource inheritanceSource = getResourceFromInheritanceChain();
+ if (inheritanceSource != null) {
+ return !StringUtils.equals(resolvedConfigurationResource.getPath(), inheritanceSource.getPath());
+ }
+ else {
+ return false;
+ }
+ }
+ }
+
+ private Resource getResourceFromInheritanceChain() {
+ if (configurationResourceInheritanceChain == null) {
+ return null;
+ }
+ return getResourceFromInheritanceChain(configurationResourceInheritanceChain.iterator());
+ }
+
+ private Resource getResourceFromInheritanceChain(Iterator inheritanceChain) {
+ if (!inheritanceChain.hasNext()) {
+ return null;
+ }
+ Resource resource = inheritanceChain.next();
+ Object valueFromResource = resource.getValueMap().get(name, effectiveValue.getClass());
+ if (valueFromResource != null) {
+ return resource;
+ }
+ return getResourceFromInheritanceChain(inheritanceChain);
+ }
+
+ @Override
+ public boolean isOverridden() {
+ if (contextResource == null) {
+ return false;
+ }
+ if (isAllOverridden) {
+ return true;
+ }
+ Map overrideProperties = configurationOverrideMultiplexer.overrideProperties(
+ contextResource.getPath(), configName, Collections.emptyMap());
+ if (overrideProperties != null) {
+ return overrideProperties.containsKey(name)
+ || (getValue() != null && effectiveValue == null);
+ }
+ else {
+ return false;
+ }
+ }
+
+}
diff --git a/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/console/CAConfigInventoryPrinter.java b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/console/CAConfigInventoryPrinter.java
new file mode 100644
index 000000000..d477b2ba7
--- /dev/null
+++ b/Java-base/sling-org-apache-sling-caconfig-impl/src/src/main/java/org/apache/sling/caconfig/management/impl/console/CAConfigInventoryPrinter.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.caconfig.management.impl.console;
+
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.felix.inventory.Format;
+import org.apache.felix.inventory.InventoryPrinter;
+import org.apache.sling.caconfig.resource.spi.CollectionInheritanceDecider;
+import org.apache.sling.caconfig.resource.spi.ConfigurationResourceResolvingStrategy;
+import org.apache.sling.caconfig.resource.spi.ContextPathStrategy;
+import org.apache.sling.caconfig.spi.ConfigurationInheritanceStrategy;
+import org.apache.sling.caconfig.spi.ConfigurationMetadataProvider;
+import org.apache.sling.caconfig.spi.ConfigurationOverrideProvider;
+import org.apache.sling.caconfig.spi.ConfigurationPersistenceStrategy2;
+import org.apache.sling.commons.osgi.Order;
+import org.apache.sling.commons.osgi.ServiceUtil;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * Web console configuration printer.
+ */
+@Component(service=InventoryPrinter.class,
+property={Constants.SERVICE_DESCRIPTION + "=Apache Sling Context-Aware Configuration Resolver Console Inventory Printer",
+ InventoryPrinter.NAME + "=" + CAConfigInventoryPrinter.NAME,
+ InventoryPrinter.TITLE + "=" + CAConfigInventoryPrinter.TITLE,
+ InventoryPrinter.FORMAT + "=TEXT"})
+public class CAConfigInventoryPrinter implements InventoryPrinter {
+
+ public static final String NAME = "slingcaconfig";
+ public static final String TITLE = "Sling Context-Aware Configuration";
+
+ private BundleContext bundleContext;
+
+ @Activate
+ private void activate(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
+
+ @Override
+ public void print(PrintWriter pw, Format format, boolean isZip) {
+ if (format != Format.TEXT) {
+ return;
+ }
+
+ printSPISection(pw, ContextPathStrategy.class, "Context Path Strategies");
+ printSPISection(pw, ConfigurationResourceResolvingStrategy.class, "Configuration Resource Resolving Strategies");
+ printSPISection(pw, CollectionInheritanceDecider.class, "Collection Inheritance Deciders");
+ printSPISection(pw, ConfigurationInheritanceStrategy.class, "Configuration Inheritance Strategies");
+ printSPISection(pw, ConfigurationPersistenceStrategy2.class, "Configuration Persistance Strategies",
+ new ConfigurationPersistenceStrategyPrinter());
+ printSPISection(pw, ConfigurationMetadataProvider.class, "Configuration Metadata Providers",
+ new ConfigurationMetadataPrinter());
+ printSPISection(pw, ConfigurationOverrideProvider.class, "Configuration Override Providers",
+ new ConfigurationOverridePrinter());
+ }
+
+ @SafeVarargs
+ private final void printSPISection(PrintWriter pw, Class clazz, String title, ServiceConfigurationPrinter... serviceConfigPrinters) {
+ Collection> serviceReferences = getServiceReferences(clazz);
+
+ pw.println(title);
+ pw.println(StringUtils.repeat('-', title.length()));
+
+ if (serviceReferences.isEmpty()) {
+ pw.println("(none)");
+ }
+ else {
+ for (ServiceReference serviceReference : serviceReferences) {
+ pw.print(ServiceConfigurationPrinter.BULLET);
+ pw.print(getServiceClassName(serviceReference));
+ pw.print(" [");
+ pw.print(getServiceRanking(serviceReference));
+ if (!isEnabled(serviceReference)) {
+ pw.print(", disabled");
+ }
+ pw.print("]");
+ pw.println();
+ for (ServiceConfigurationPrinter serviceConfigPrinter : serviceConfigPrinters) {
+ serviceConfigPrinter.printConfiguration(pw, serviceReference, bundleContext);
+ }
+ }
+ }
+ pw.println();
+ }
+
+ private Collection