/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sling.caconfig.impl.def;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.caconfig.management.ConfigurationManagementSettings;
import org.apache.sling.caconfig.management.impl.PropertiesFilterUtil;
import org.apache.sling.caconfig.resource.impl.util.MapUtil;
import org.apache.sling.caconfig.spi.ConfigurationCollectionPersistData;
import org.apache.sling.caconfig.spi.ConfigurationPersistData;
import org.apache.sling.caconfig.spi.ConfigurationPersistenceAccessDeniedException;
import org.apache.sling.caconfig.spi.ConfigurationPersistenceException;
import org.apache.sling.caconfig.spi.ConfigurationPersistenceStrategy2;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The default persistence strategy is quite simple: directly use the configuration resources.
* All existing properties are removed when new properties are stored in a singleton config resource.
* All existing child resources are removed when a new configs are stored for collection config resources.
*/
@Component(service = ConfigurationPersistenceStrategy2.class)
@Designate(ocd=DefaultConfigurationPersistenceStrategy.Config.class)
public class DefaultConfigurationPersistenceStrategy implements ConfigurationPersistenceStrategy2 {
@ObjectClassDefinition(name="Apache Sling Context-Aware Configuration Default Resource Persistence Strategy",
description="Directly uses configuration resources for storing configuration data.")
static @interface Config {
@AttributeDefinition(name="Enabled",
description = "Enable this configuration resource persistence strategy.")
boolean enabled() default true;
}
@Reference
private ConfigurationManagementSettings configurationManagementSettings;
private volatile Config config;
private static final Logger log = LoggerFactory.getLogger(DefaultConfigurationPersistenceStrategy.class);
@Activate
private void activate(ComponentContext componentContext, Config config) {
this.config = config;
}
@Override
public Resource getResource(Resource resource) {
if (!config.enabled()) {
return null;
}
return resource;
}
@Override
public Resource getCollectionParentResource(Resource resource) {
if (!config.enabled()) {
return null;
}
return resource;
}
@Override
public Resource getCollectionItemResource(Resource resource) {
if (!config.enabled()) {
return null;
}
return resource;
}
@Override
public String getResourcePath(String resourcePath) {
if (!config.enabled()) {
return null;
}
return resourcePath;
}
@Override
public String getCollectionParentResourcePath(String resourcePath) {
if (!config.enabled()) {
return null;
}
return resourcePath;
}
@Override
public String getCollectionItemResourcePath(String resourcePath) {
if (!config.enabled()) {
return null;
}
return resourcePath;
}
@Override
public String getConfigName(String configName, String relatedConfigPath) {
if (!config.enabled()) {
return null;
}
return configName;
}
@Override
public String getCollectionParentConfigName(String configName, String relatedConfigPath) {
if (!config.enabled()) {
return null;
}
return configName;
}
@Override
public String getCollectionItemConfigName(String configName, String relatedConfigPath) {
if (!config.enabled()) {
return null;
}
return configName;
}
@Override
public boolean persistConfiguration(ResourceResolver resourceResolver, String configResourcePath,
ConfigurationPersistData data) {
if (!config.enabled()) {
return false;
}
getOrCreateResource(resourceResolver, configResourcePath, data.getProperties());
commit(resourceResolver, configResourcePath);
return true;
}
@Override
public boolean persistConfigurationCollection(ResourceResolver resourceResolver, String configResourceCollectionParentPath,
ConfigurationCollectionPersistData data) {
if (!config.enabled()) {
return false;
}
Resource configResourceParent = getOrCreateResource(resourceResolver, configResourceCollectionParentPath, data.getProperties());
// delete existing children and create new ones
deleteChildren(configResourceParent);
for (ConfigurationPersistData item : data.getItems()) {
String path = configResourceParent.getPath() + "/" + item.getCollectionItemName();
getOrCreateResource(resourceResolver, path, item.getProperties());
}
commit(resourceResolver, configResourceCollectionParentPath);
return true;
}
@Override
public boolean deleteConfiguration(ResourceResolver resourceResolver, String configResourcePath) {
if (!config.enabled()) {
return false;
}
Resource resource = resourceResolver.getResource(configResourcePath);
if (resource != null) {
try {
log.trace("! Delete resource {}", resource.getPath());
resourceResolver.delete(resource);
}
catch (PersistenceException ex) {
throw convertPersistenceException("Unable to delete configuration at " + configResourcePath, ex);
}
}
commit(resourceResolver, configResourcePath);
return true;
}
private Resource getOrCreateResource(ResourceResolver resourceResolver, String path, Map<String,Object> properties) {
try {
Resource resource = ResourceUtil.getOrCreateResource(resourceResolver, path, (String)null, (String)null, false);
if (properties != null) {
replaceProperties(resource, properties);
}
return resource;
}
catch (PersistenceException ex) {
throw convertPersistenceException("Unable to persist configuration to " + path, ex);
}
}
private void deleteChildren(Resource resource) {
ResourceResolver resourceResolver = resource.getResourceResolver();
try {
for (Resource child : resource.getChildren()) {
log.trace("! Delete resource {}", child.getPath());
resourceResolver.delete(child);
}
}
catch (PersistenceException ex) {
throw convertPersistenceException("Unable to remove children from " + resource.getPath(), ex);
}
}
private void replaceProperties(Resource resource, Map<String,Object> properties) {
if (log.isTraceEnabled()) {
log.trace("! Store properties for resource {}: {}", resource.getPath(), MapUtil.traceOutput(properties));
}
ModifiableValueMap modValueMap = resource.adaptTo(ModifiableValueMap.class);
if (modValueMap == null) {
throw new ConfigurationPersistenceAccessDeniedException("No write access: Unable to store configuration data to " + resource.getPath() + ".");
}
// remove all existing properties that are not filterd
Set<String> propertyNamesToRemove = new HashSet<>(modValueMap.keySet());
PropertiesFilterUtil.removeIgnoredProperties(propertyNamesToRemove, configurationManagementSettings);
for (String propertyName : propertyNamesToRemove) {
modValueMap.remove(propertyName);
}
modValueMap.putAll(properties);
}
private void commit(ResourceResolver resourceResolver, String relatedResourcePath) {
try {
resourceResolver.commit();
}
catch (PersistenceException ex) {
throw convertPersistenceException("Unable to persist configuration changes to " + relatedResourcePath, ex);
}
}
private ConfigurationPersistenceException convertPersistenceException(String message, PersistenceException ex) {
if (StringUtils.equals(ex.getCause().getClass().getName(), "javax.jcr.AccessDeniedException")) {
// detect if commit failed due to read-only access to repository
return new ConfigurationPersistenceAccessDeniedException("No write access: " + message, ex);
}
return new ConfigurationPersistenceException(message, ex);
}
}