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 <stefanseif...@users.noreply.github.com>
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();
+    }
+
+}


Reply via email to