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(); + } + +}