This is an automated email from the ASF dual-hosted git repository.
sseifert pushed a commit to branch master
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-caconfig-mock-plugin.git
The following commit(s) were added to refs/heads/master by this push:
new ee86a7f SLING-12192 Fix nested config path building when writing
config with custom persistence strategies
ee86a7f is described below
commit ee86a7f1c858af337b06b086473462ea9b283b7d
Author: Stefan Seifert <[email protected]>
AuthorDate: Fri Dec 8 14:10:27 2023 +0100
SLING-12192 Fix nested config path building when writing config with custom
persistence strategies
---
pom.xml | 6 +-
.../mock/caconfig/ConfigurationPersistHelper.java | 27 +--
.../mock/caconfig/MockContextAwareConfigTest.java | 2 +-
...kContextAwareConfig_CustomPersistence2Test.java | 37 ++++
...kContextAwareConfig_CustomPersistence3Test.java | 37 ++++
...ckContextAwareConfig_CustomPersistenceTest.java | 37 ++++
.../CustomConfigurationPersistenceStrategy.java | 193 +++++++++++++++++
.../CustomConfigurationPersistenceStrategy2.java | 235 +++++++++++++++++++++
.../CustomConfigurationPersistenceStrategy3.java | 211 ++++++++++++++++++
9 files changed, 769 insertions(+), 16 deletions(-)
diff --git a/pom.xml b/pom.xml
index 200c1a1..9e25e5b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,19 +77,19 @@
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.caconfig.api</artifactId>
- <version>1.1.0</version>
+ <version>1.1.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.caconfig.spi</artifactId>
- <version>1.3.0</version>
+ <version>1.3.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.caconfig.impl</artifactId>
- <version>1.4.0</version>
+ <version>1.4.14</version>
<scope>compile</scope>
</dependency>
<dependency>
diff --git
a/src/main/java/org/apache/sling/testing/mock/caconfig/ConfigurationPersistHelper.java
b/src/main/java/org/apache/sling/testing/mock/caconfig/ConfigurationPersistHelper.java
index 87d8c26..988d33e 100644
---
a/src/main/java/org/apache/sling/testing/mock/caconfig/ConfigurationPersistHelper.java
+++
b/src/main/java/org/apache/sling/testing/mock/caconfig/ConfigurationPersistHelper.java
@@ -24,6 +24,7 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.caconfig.management.ConfigurationManager;
import
org.apache.sling.caconfig.management.multiplexer.ConfigurationPersistenceStrategyMultiplexer;
@@ -107,22 +108,24 @@ class ConfigurationPersistHelper {
}
}
+ private String getConfigName(@NotNull String configName) {
+ return
StringUtils.defaultString(configurationPersistenceStrategy.getConfigName(configName,
null), configName);
+ }
+
+ private String getCollectionParentConfigName(@NotNull String configName) {
+ return
StringUtils.defaultString(configurationPersistenceStrategy.getCollectionParentConfigName(configName,
null), configName);
+ }
+
+ private String getCollectionItemConfigName(@NotNull String configName) {
+ return
StringUtils.defaultString(configurationPersistenceStrategy.getCollectionItemConfigName(configName,
null), configName);
+ }
+
private String getNestedConfigName(@NotNull String configName, @NotNull
String key) {
- String nestedKey = configName + "/" + key;
- String nestedConfigName =
configurationPersistenceStrategy.getConfigName(nestedKey, null);
- if (nestedConfigName == null) {
- throw new IllegalArgumentException("Nested configuration not
supported for " + nestedKey);
- }
- return nestedConfigName;
+ return getConfigName(configName) + "/" + key;
}
private String getNestedCollectionItemConfigName(@NotNull String
configName, @NotNull String itemName, @NotNull String key) {
- String nestedKey = configName + "/" + itemName + "/" + key;
- String nestedConfigName =
configurationPersistenceStrategy.getCollectionItemConfigName(nestedKey, null);
- if (nestedConfigName == null) {
- throw new IllegalArgumentException("Nested configuration not
supported for " + nestedKey);
- }
- return nestedConfigName;
+ return
getCollectionItemConfigName(getCollectionParentConfigName(configName) + "/" +
itemName) + "/" + key;
}
}
diff --git
a/src/test/java/org/apache/sling/testing/mock/caconfig/MockContextAwareConfigTest.java
b/src/test/java/org/apache/sling/testing/mock/caconfig/MockContextAwareConfigTest.java
index 9170091..92de0c9 100644
---
a/src/test/java/org/apache/sling/testing/mock/caconfig/MockContextAwareConfigTest.java
+++
b/src/test/java/org/apache/sling/testing/mock/caconfig/MockContextAwareConfigTest.java
@@ -151,7 +151,7 @@ public class MockContextAwareConfigTest {
}
@Test
- public void testNestedCollectionConfigConfig() {
+ public void testNestedCollectionConfig() {
MockContextAwareConfig.writeConfigurationCollection(context,
"/content/region/site", NestedListConfig.class, List.of(
Map.of("stringParam", "value1",
"sub", List.of(
diff --git
a/src/test/java/org/apache/sling/testing/mock/caconfig/MockContextAwareConfig_CustomPersistence2Test.java
b/src/test/java/org/apache/sling/testing/mock/caconfig/MockContextAwareConfig_CustomPersistence2Test.java
new file mode 100644
index 0000000..77ae670
--- /dev/null
+++
b/src/test/java/org/apache/sling/testing/mock/caconfig/MockContextAwareConfig_CustomPersistence2Test.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.testing.mock.caconfig;
+
+import org.apache.sling.caconfig.spi.ConfigurationPersistenceStrategy2;
+import
org.apache.sling.testing.mock.caconfig.persistence.CustomConfigurationPersistenceStrategy2;
+import org.junit.Before;
+import org.osgi.framework.Constants;
+
+public class MockContextAwareConfig_CustomPersistence2Test extends
MockContextAwareConfigTest {
+
+ @Override
+ @Before
+ public void setUp() {
+ context.registerService(ConfigurationPersistenceStrategy2.class, new
CustomConfigurationPersistenceStrategy2(),
+ Constants.SERVICE_RANKING, 2000);
+
+ super.setUp();
+ }
+
+}
diff --git
a/src/test/java/org/apache/sling/testing/mock/caconfig/MockContextAwareConfig_CustomPersistence3Test.java
b/src/test/java/org/apache/sling/testing/mock/caconfig/MockContextAwareConfig_CustomPersistence3Test.java
new file mode 100644
index 0000000..4fabda3
--- /dev/null
+++
b/src/test/java/org/apache/sling/testing/mock/caconfig/MockContextAwareConfig_CustomPersistence3Test.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.testing.mock.caconfig;
+
+import org.apache.sling.caconfig.spi.ConfigurationPersistenceStrategy2;
+import
org.apache.sling.testing.mock.caconfig.persistence.CustomConfigurationPersistenceStrategy3;
+import org.junit.Before;
+import org.osgi.framework.Constants;
+
+public class MockContextAwareConfig_CustomPersistence3Test extends
MockContextAwareConfigTest {
+
+ @Override
+ @Before
+ public void setUp() {
+ context.registerService(ConfigurationPersistenceStrategy2.class, new
CustomConfigurationPersistenceStrategy3(),
+ Constants.SERVICE_RANKING, 2000);
+
+ super.setUp();
+ }
+
+}
diff --git
a/src/test/java/org/apache/sling/testing/mock/caconfig/MockContextAwareConfig_CustomPersistenceTest.java
b/src/test/java/org/apache/sling/testing/mock/caconfig/MockContextAwareConfig_CustomPersistenceTest.java
new file mode 100644
index 0000000..5278ad5
--- /dev/null
+++
b/src/test/java/org/apache/sling/testing/mock/caconfig/MockContextAwareConfig_CustomPersistenceTest.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.testing.mock.caconfig;
+
+import org.apache.sling.caconfig.spi.ConfigurationPersistenceStrategy2;
+import
org.apache.sling.testing.mock.caconfig.persistence.CustomConfigurationPersistenceStrategy;
+import org.junit.Before;
+import org.osgi.framework.Constants;
+
+public class MockContextAwareConfig_CustomPersistenceTest extends
MockContextAwareConfigTest {
+
+ @Override
+ @Before
+ public void setUp() {
+ context.registerService(ConfigurationPersistenceStrategy2.class, new
CustomConfigurationPersistenceStrategy(),
+ Constants.SERVICE_RANKING, 2000);
+
+ super.setUp();
+ }
+
+}
diff --git
a/src/test/java/org/apache/sling/testing/mock/caconfig/persistence/CustomConfigurationPersistenceStrategy.java
b/src/test/java/org/apache/sling/testing/mock/caconfig/persistence/CustomConfigurationPersistenceStrategy.java
new file mode 100644
index 0000000..6d76318
--- /dev/null
+++
b/src/test/java/org/apache/sling/testing/mock/caconfig/persistence/CustomConfigurationPersistenceStrategy.java
@@ -0,0 +1,193 @@
+/*
+ * 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.testing.mock.caconfig.persistence;
+
+import static org.junit.Assert.assertNotNull;
+
+import java.util.HashSet;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.JcrConstants;
+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.api.resource.ValueMap;
+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.ConfigurationPersistenceStrategy2;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This is a variant of {@link
org.apache.sling.caconfig.impl.def.DefaultConfigurationPersistenceStrategy}
+ * which reads and stores data from a sub-resources named "jcr:content".
+ */
+public class CustomConfigurationPersistenceStrategy implements
ConfigurationPersistenceStrategy2 {
+
+ private static final String DEFAULT_RESOURCE_TYPE =
JcrConstants.NT_UNSTRUCTURED;
+ private static final String CHILD_NODE_NAME = JcrConstants.JCR_CONTENT;
+
+ @Override
+ public Resource getResource(@NotNull Resource resource) {
+ assertNotNull(resource);
+ return resource.getChild(CHILD_NODE_NAME);
+ }
+
+ @Override
+ public Resource getCollectionParentResource(@NotNull Resource resource) {
+ assertNotNull(resource);
+ return resource;
+ }
+
+ @Override
+ public Resource getCollectionItemResource(@NotNull Resource resource) {
+ assertNotNull(resource);
+ return resource.getChild(CHILD_NODE_NAME);
+ }
+
+ @Override
+ public String getResourcePath(@NotNull String resourcePath) {
+ assertNotNull(resourcePath);
+ return resourcePath + "/" + CHILD_NODE_NAME;
+ }
+
+ @Override
+ public String getCollectionParentResourcePath(@NotNull String
resourcePath) {
+ assertNotNull(resourcePath);
+ return resourcePath;
+ }
+
+ @Override
+ public String getCollectionItemResourcePath(@NotNull String resourcePath) {
+ assertNotNull(resourcePath);
+ return resourcePath + "/" + CHILD_NODE_NAME;
+ }
+
+ @Override
+ public String getConfigName(@NotNull String configName, @Nullable String
relatedConfigPath) {
+ assertNotNull(configName);
+ return configName + "/" + CHILD_NODE_NAME;
+ }
+
+ @Override
+ public String getCollectionParentConfigName(@NotNull String configName,
@Nullable String relatedConfigPath) {
+ assertNotNull(configName);
+ return configName;
+ }
+
+ @Override
+ public String getCollectionItemConfigName(@NotNull String configName,
@Nullable String relatedConfigPath) {
+ assertNotNull(configName);
+ return configName + "/" + CHILD_NODE_NAME;
+ }
+
+ @Override
+ public boolean persistConfiguration(@NotNull ResourceResolver
resourceResolver, @NotNull String configResourcePath,
+ @NotNull ConfigurationPersistData data) {
+ getOrCreateResource(resourceResolver, configResourcePath + "/" +
CHILD_NODE_NAME, data.getProperties());
+ commit(resourceResolver);
+ return true;
+ }
+
+ @Override
+ public boolean persistConfigurationCollection(@NotNull ResourceResolver
resourceResolver, @NotNull String configResourceCollectionParentPath,
+ @NotNull ConfigurationCollectionPersistData data) {
+ Resource configResourceParent = getOrCreateResource(resourceResolver,
configResourceCollectionParentPath, ValueMap.EMPTY);
+
+ // delete existing children and create new ones
+ deleteChildren(configResourceParent);
+ for (ConfigurationPersistData item : data.getItems()) {
+ String path = configResourceParent.getPath() + "/" +
item.getCollectionItemName() + "/" + CHILD_NODE_NAME;
+ getOrCreateResource(resourceResolver, path, item.getProperties());
+ }
+
+ // if resource collection parent properties are given replace them as
well
+ if (data.getProperties() != null) {
+ replaceProperties(configResourceParent, data.getProperties());
+ }
+
+ commit(resourceResolver);
+ return true;
+ }
+
+ @Override
+ public boolean deleteConfiguration(@NotNull ResourceResolver
resourceResolver, @NotNull String configResourcePath) {
+ Resource resource = resourceResolver.getResource(configResourcePath);
+ if (resource != null) {
+ try {
+ resourceResolver.delete(resource);
+ }
+ catch (PersistenceException ex) {
+ throw new ConfigurationPersistenceException("Unable to delete
configuration at " + configResourcePath, ex);
+ }
+ }
+ commit(resourceResolver);
+ return true;
+ }
+
+ private Resource getOrCreateResource(ResourceResolver resourceResolver,
String path, Map<String,Object> properties) {
+ try {
+ Resource resource =
ResourceUtil.getOrCreateResource(resourceResolver, path, DEFAULT_RESOURCE_TYPE,
DEFAULT_RESOURCE_TYPE, false);
+ replaceProperties(resource, properties);
+ return resource;
+ }
+ catch (PersistenceException ex) {
+ throw new ConfigurationPersistenceException("Unable to persist
configuration to " + path, ex);
+ }
+ }
+
+ private void deleteChildren(Resource resource) {
+ ResourceResolver resourceResolver = resource.getResourceResolver();
+ try {
+ for (Resource child : resource.getChildren()) {
+ resourceResolver.delete(child);
+ }
+ }
+ catch (PersistenceException ex) {
+ throw new ConfigurationPersistenceException("Unable to remove
children from " + resource.getPath(), ex);
+ }
+ }
+
+ @SuppressWarnings("null")
+ private void replaceProperties(Resource resource, Map<String,Object>
properties) {
+ ModifiableValueMap modValueMap =
resource.adaptTo(ModifiableValueMap.class);
+ // remove all existing properties that do not have jcr: namespace
+ for (String propertyName : new HashSet<>(modValueMap.keySet())) {
+ if (StringUtils.startsWith(propertyName, "jcr:")) {
+ continue;
+ }
+ modValueMap.remove(propertyName);
+ }
+ modValueMap.putAll(properties);
+ }
+
+ private void commit(ResourceResolver resourceResolver) {
+ try {
+ resourceResolver.commit();
+ }
+ catch (PersistenceException ex) {
+ throw new ConfigurationPersistenceException("Unable to save
configuration: " + ex.getMessage(), ex);
+ }
+ }
+
+}
diff --git
a/src/test/java/org/apache/sling/testing/mock/caconfig/persistence/CustomConfigurationPersistenceStrategy2.java
b/src/test/java/org/apache/sling/testing/mock/caconfig/persistence/CustomConfigurationPersistenceStrategy2.java
new file mode 100644
index 0000000..1f9cb38
--- /dev/null
+++
b/src/test/java/org/apache/sling/testing/mock/caconfig/persistence/CustomConfigurationPersistenceStrategy2.java
@@ -0,0 +1,235 @@
+/*
+ * 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.testing.mock.caconfig.persistence;
+
+import static org.junit.Assert.assertNotNull;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.JcrConstants;
+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.api.resource.ValueMap;
+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.ConfigurationPersistenceStrategy2;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This is a variant of {@link
org.apache.sling.caconfig.impl.def.DefaultConfigurationPersistenceStrategy}
+ * which reads and stores data from a sub-resources named "jcr:content".
+ *
+ * Difference to {@link CustomConfigurationPersistenceStrategy}:
+ * - For configuration collections jcr:content is added only for the parent,
not for each item
+ * - For nested configuration jcr:content is not duplicated in the path
+ */
+public class CustomConfigurationPersistenceStrategy2 implements
ConfigurationPersistenceStrategy2 {
+
+ private static final String DEFAULT_RESOURCE_TYPE =
JcrConstants.NT_UNSTRUCTURED;
+ private static final String CHILD_NODE_NAME = JcrConstants.JCR_CONTENT;
+ private static final Pattern JCR_CONTENT_PATTERN =
Pattern.compile("(.*/)?" + Pattern.quote(CHILD_NODE_NAME) + "(/.*)?");
+
+ @Override
+ public Resource getResource(@NotNull Resource resource) {
+ assertNotNull(resource);
+ if (containsJcrContent(resource.getPath())) {
+ return resource;
+ }
+ else {
+ return resource.getChild(CHILD_NODE_NAME);
+ }
+ }
+
+ @Override
+ public Resource getCollectionParentResource(@NotNull Resource resource) {
+ assertNotNull(resource);
+ if (containsJcrContent(resource.getPath())) {
+ return resource;
+ }
+ else {
+ return resource.getChild(CHILD_NODE_NAME);
+ }
+ }
+
+ @Override
+ public Resource getCollectionItemResource(@NotNull Resource resource) {
+ assertNotNull(resource);
+ return resource;
+ }
+
+ @Override
+ public String getResourcePath(@NotNull String resourcePath) {
+ assertNotNull(resourcePath);
+ if (containsJcrContent(resourcePath)) {
+ return resourcePath;
+ }
+ else {
+ return resourcePath + "/" + CHILD_NODE_NAME;
+ }
+ }
+
+ @Override
+ public String getCollectionParentResourcePath(@NotNull String
resourcePath) {
+ assertNotNull(resourcePath);
+ if (containsJcrContent(resourcePath)) {
+ return resourcePath;
+ }
+ else {
+ return resourcePath + "/" + CHILD_NODE_NAME;
+ }
+ }
+
+ @Override
+ public String getCollectionItemResourcePath(@NotNull String resourcePath) {
+ assertNotNull(resourcePath);
+ return resourcePath;
+ }
+
+ @Override
+ public String getConfigName(@NotNull String configName, @Nullable String
relatedConfigPath) {
+ assertNotNull(configName);
+ if (containsJcrContent(configName)) {
+ return configName;
+ }
+ else {
+ return configName + "/" + CHILD_NODE_NAME;
+ }
+ }
+
+ @Override
+ public String getCollectionParentConfigName(@NotNull String configName,
@Nullable String relatedConfigPath) {
+ assertNotNull(configName);
+ if (containsJcrContent(configName)) {
+ return configName;
+ }
+ else {
+ return configName + "/" + CHILD_NODE_NAME;
+ }
+ }
+
+ @Override
+ public String getCollectionItemConfigName(@NotNull String configName,
@Nullable String relatedConfigPath) {
+ assertNotNull(configName);
+ return configName;
+ }
+
+ @Override
+ public boolean persistConfiguration(@NotNull ResourceResolver
resourceResolver, @NotNull String configResourcePath,
+ @NotNull ConfigurationPersistData data) {
+ getOrCreateResource(resourceResolver,
getResourcePath(configResourcePath), data.getProperties());
+ commit(resourceResolver);
+ return true;
+ }
+
+ @Override
+ public boolean persistConfigurationCollection(@NotNull ResourceResolver
resourceResolver, @NotNull String configResourceCollectionParentPath,
+ @NotNull ConfigurationCollectionPersistData data) {
+ String parentPath =
getCollectionParentResourcePath(configResourceCollectionParentPath);
+ Resource configResourceParent = getOrCreateResource(resourceResolver,
parentPath, ValueMap.EMPTY);
+
+ // delete existing children and create new ones
+ deleteChildren(configResourceParent);
+ for (ConfigurationPersistData item : data.getItems()) {
+ String path =
getCollectionItemResourcePath(configResourceParent.getPath() + "/" +
item.getCollectionItemName());
+ getOrCreateResource(resourceResolver, path, item.getProperties());
+ }
+
+ // if resource collection parent properties are given replace them as
well
+ if (data.getProperties() != null) {
+ Resource propsResource = getOrCreateResource(resourceResolver,
parentPath + "/colPropsResource", ValueMap.EMPTY);
+ replaceProperties(propsResource, data.getProperties());
+ }
+
+ commit(resourceResolver);
+ return true;
+ }
+
+ @Override
+ public boolean deleteConfiguration(@NotNull ResourceResolver
resourceResolver, @NotNull String configResourcePath) {
+ Resource resource = resourceResolver.getResource(configResourcePath);
+ if (resource != null) {
+ try {
+ resourceResolver.delete(resource);
+ }
+ catch (PersistenceException ex) {
+ throw new ConfigurationPersistenceException("Unable to delete
configuration at " + configResourcePath, ex);
+ }
+ }
+ commit(resourceResolver);
+ return true;
+ }
+
+ private Resource getOrCreateResource(ResourceResolver resourceResolver,
String path, Map<String,Object> properties) {
+ try {
+ Resource resource =
ResourceUtil.getOrCreateResource(resourceResolver, path, DEFAULT_RESOURCE_TYPE,
DEFAULT_RESOURCE_TYPE, false);
+ replaceProperties(resource, properties);
+ return resource;
+ }
+ catch (PersistenceException ex) {
+ throw new ConfigurationPersistenceException("Unable to persist
configuration to " + path, ex);
+ }
+ }
+
+ private void deleteChildren(Resource resource) {
+ ResourceResolver resourceResolver = resource.getResourceResolver();
+ try {
+ for (Resource child : resource.getChildren()) {
+ resourceResolver.delete(child);
+ }
+ }
+ catch (PersistenceException ex) {
+ throw new ConfigurationPersistenceException("Unable to remove
children from " + resource.getPath(), ex);
+ }
+ }
+
+ @SuppressWarnings("null")
+ private void replaceProperties(Resource resource, Map<String,Object>
properties) {
+ ModifiableValueMap modValueMap =
resource.adaptTo(ModifiableValueMap.class);
+ // remove all existing properties that do not have jcr: namespace
+ for (String propertyName : new HashSet<>(modValueMap.keySet())) {
+ if (StringUtils.startsWith(propertyName, "jcr:")) {
+ continue;
+ }
+ modValueMap.remove(propertyName);
+ }
+ modValueMap.putAll(properties);
+ }
+
+ private void commit(ResourceResolver resourceResolver) {
+ try {
+ resourceResolver.commit();
+ }
+ catch (PersistenceException ex) {
+ throw new ConfigurationPersistenceException("Unable to save
configuration: " + ex.getMessage(), ex);
+ }
+ }
+
+ static boolean containsJcrContent(String path) {
+ return JCR_CONTENT_PATTERN.matcher(path).matches();
+ }
+
+}
diff --git
a/src/test/java/org/apache/sling/testing/mock/caconfig/persistence/CustomConfigurationPersistenceStrategy3.java
b/src/test/java/org/apache/sling/testing/mock/caconfig/persistence/CustomConfigurationPersistenceStrategy3.java
new file mode 100644
index 0000000..3641adb
--- /dev/null
+++
b/src/test/java/org/apache/sling/testing/mock/caconfig/persistence/CustomConfigurationPersistenceStrategy3.java
@@ -0,0 +1,211 @@
+/*
+ * 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.testing.mock.caconfig.persistence;
+
+import static org.junit.Assert.assertNotNull;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.JcrConstants;
+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.api.resource.ValueMap;
+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.ConfigurationPersistenceStrategy2;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * This is a variant of {@link
org.apache.sling.caconfig.impl.def.DefaultConfigurationPersistenceStrategy}
+ * which reads and stores data from a sub-resources named "jcr:content".
+ *
+ * Difference to {@link CustomConfigurationPersistenceStrategy}:
+ * - For configuration collections jcr:content is added for each item, not for
the parent
+ * - For nested configuration jcr:content is not duplicated in the path
+ */
+public class CustomConfigurationPersistenceStrategy3 implements
ConfigurationPersistenceStrategy2 {
+
+ private static final String DEFAULT_RESOURCE_TYPE =
JcrConstants.NT_UNSTRUCTURED;
+ private static final String CHILD_NODE_NAME = JcrConstants.JCR_CONTENT;
+ private static final Pattern JCR_CONTENT_PATTERN =
Pattern.compile("(.*/)?" + Pattern.quote(CHILD_NODE_NAME) + "(/.*)?");
+
+ @Override
+ public Resource getResource(@NotNull Resource resource) {
+ assertNotNull(resource);
+ if (containsJcrContent(resource.getPath())) {
+ return resource;
+ }
+ return resource.getChild(CHILD_NODE_NAME);
+ }
+
+ @Override
+ public Resource getCollectionParentResource(@NotNull Resource resource) {
+ assertNotNull(resource);
+ return resource;
+ }
+
+ @Override
+ public Resource getCollectionItemResource(@NotNull Resource resource) {
+ return getResource(resource);
+ }
+
+ @Override
+ public String getResourcePath(@NotNull String resourcePath) {
+ assertNotNull(resourcePath);
+ if (containsJcrContent(resourcePath)) {
+ return resourcePath;
+ }
+ return resourcePath + "/" + CHILD_NODE_NAME;
+ }
+
+ @Override
+ public String getCollectionParentResourcePath(@NotNull String
resourcePath) {
+ assertNotNull(resourcePath);
+ return resourcePath;
+ }
+
+ @Override
+ public String getCollectionItemResourcePath(@NotNull String resourcePath) {
+ return getResourcePath(resourcePath);
+ }
+
+ @Override
+ public String getConfigName(@NotNull String configName, @Nullable String
relatedConfigPath) {
+ assertNotNull(configName);
+ if (containsJcrContent(configName)) {
+ return configName;
+ }
+ return configName + "/" + CHILD_NODE_NAME;
+ }
+
+ @Override
+ public String getCollectionParentConfigName(@NotNull String configName,
@Nullable String relatedConfigPath) {
+ assertNotNull(configName);
+ return configName;
+ }
+
+ @Override
+ public String getCollectionItemConfigName(@NotNull String configName,
@Nullable String relatedConfigPath) {
+ return getConfigName(configName, relatedConfigPath);
+ }
+
+ @Override
+ public boolean persistConfiguration(@NotNull ResourceResolver
resourceResolver, @NotNull String configResourcePath,
+ @NotNull ConfigurationPersistData data) {
+ getOrCreateResource(resourceResolver,
getResourcePath(configResourcePath), data.getProperties());
+ commit(resourceResolver);
+ return true;
+ }
+
+ @Override
+ public boolean persistConfigurationCollection(@NotNull ResourceResolver
resourceResolver, @NotNull String configResourceCollectionParentPath,
+ @NotNull ConfigurationCollectionPersistData data) {
+ String parentPath =
getCollectionParentResourcePath(configResourceCollectionParentPath);
+ Resource configResourceParent = getOrCreateResource(resourceResolver,
parentPath, ValueMap.EMPTY);
+
+ // delete existing children and create new ones
+ deleteChildren(configResourceParent);
+ for (ConfigurationPersistData item : data.getItems()) {
+ String path =
getCollectionItemResourcePath(configResourceParent.getPath() + "/" +
item.getCollectionItemName());
+ getOrCreateResource(resourceResolver, path, item.getProperties());
+ }
+
+ // if resource collection parent properties are given replace them as
well
+ if (data.getProperties() != null) {
+ Resource propsResource = getOrCreateResource(resourceResolver,
parentPath + "/colPropsResource", ValueMap.EMPTY);
+ replaceProperties(propsResource, data.getProperties());
+ }
+
+ commit(resourceResolver);
+ return true;
+ }
+
+ @Override
+ public boolean deleteConfiguration(@NotNull ResourceResolver
resourceResolver, @NotNull String configResourcePath) {
+ Resource resource = resourceResolver.getResource(configResourcePath);
+ if (resource != null) {
+ try {
+ resourceResolver.delete(resource);
+ }
+ catch (PersistenceException ex) {
+ throw new ConfigurationPersistenceException("Unable to delete
configuration at " + configResourcePath, ex);
+ }
+ }
+ commit(resourceResolver);
+ return true;
+ }
+
+ private Resource getOrCreateResource(ResourceResolver resourceResolver,
String path, Map<String,Object> properties) {
+ try {
+ Resource resource =
ResourceUtil.getOrCreateResource(resourceResolver, path, DEFAULT_RESOURCE_TYPE,
DEFAULT_RESOURCE_TYPE, false);
+ replaceProperties(resource, properties);
+ return resource;
+ }
+ catch (PersistenceException ex) {
+ throw new ConfigurationPersistenceException("Unable to persist
configuration to " + path, ex);
+ }
+ }
+
+ private void deleteChildren(Resource resource) {
+ ResourceResolver resourceResolver = resource.getResourceResolver();
+ try {
+ for (Resource child : resource.getChildren()) {
+ resourceResolver.delete(child);
+ }
+ }
+ catch (PersistenceException ex) {
+ throw new ConfigurationPersistenceException("Unable to remove
children from " + resource.getPath(), ex);
+ }
+ }
+
+ @SuppressWarnings("null")
+ private void replaceProperties(Resource resource, Map<String,Object>
properties) {
+ ModifiableValueMap modValueMap =
resource.adaptTo(ModifiableValueMap.class);
+ // remove all existing properties that do not have jcr: namespace
+ for (String propertyName : new HashSet<>(modValueMap.keySet())) {
+ if (StringUtils.startsWith(propertyName, "jcr:")) {
+ continue;
+ }
+ modValueMap.remove(propertyName);
+ }
+ modValueMap.putAll(properties);
+ }
+
+ private void commit(ResourceResolver resourceResolver) {
+ try {
+ resourceResolver.commit();
+ }
+ catch (PersistenceException ex) {
+ throw new ConfigurationPersistenceException("Unable to save
configuration: " + ex.getMessage(), ex);
+ }
+ }
+
+ static boolean containsJcrContent(String path) {
+ return JCR_CONTENT_PATTERN.matcher(path).matches();
+ }
+
+}