ConfigDataServlet.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2016 wcm.io
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package io.wcm.caconfig.editor.impl;
import static io.wcm.caconfig.editor.EditorProperties.PROPERTY_DROPDOWN_OPTIONS;
import static io.wcm.caconfig.editor.EditorProperties.PROPERTY_DROPDOWN_OPTIONS_PROVIDER;
import static io.wcm.caconfig.editor.EditorProperties.PROPERTY_WIDGET_TYPE;
import static io.wcm.caconfig.editor.EditorProperties.WIDGET_TYPE_DROPDOWN;
import static io.wcm.caconfig.editor.impl.JsonMapper.OBJECT_MAPPER;
import static io.wcm.caconfig.editor.impl.NameConstants.RP_COLLECTION;
import static io.wcm.caconfig.editor.impl.NameConstants.RP_CONFIGNAME;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.TreeMap;
import java.util.regex.Pattern;
import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ClassUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.apache.sling.caconfig.management.ConfigurationCollectionData;
import org.apache.sling.caconfig.management.ConfigurationData;
import org.apache.sling.caconfig.management.ConfigurationManager;
import org.apache.sling.caconfig.management.ValueInfo;
import org.apache.sling.caconfig.management.multiplexer.ConfigurationPersistenceStrategyMultiplexer;
import org.apache.sling.caconfig.spi.ConfigurationPersistenceException;
import org.apache.sling.caconfig.spi.metadata.PropertyMetadata;
import org.apache.sling.servlets.annotations.SlingServletResourceTypes;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.wcm.caconfig.editor.impl.data.configdata.ConfigCollectionItem;
import io.wcm.caconfig.editor.impl.data.configdata.ConfigItem;
import io.wcm.caconfig.editor.impl.data.configdata.PropertyItem;
import io.wcm.caconfig.editor.impl.data.configdata.PropertyItemMetadata;
/**
* Read configuration data.
*/
@Component(service = Servlet.class)
@SlingServletResourceTypes(
resourceTypes = "/apps/wcm-io/caconfig/editor/components/page/editor",
selectors = ConfigDataServlet.SELECTOR,
extensions = "json",
methods = "GET")
public class ConfigDataServlet extends SlingSafeMethodsServlet {
private static final long serialVersionUID = 1L;
/**
* Selector
*/
public static final String SELECTOR = "configData";
private static final Pattern JSON_STRING_ARRAY_PATTERN = Pattern.compile("^\\[.*\\]$");
private static final Pattern JSON_STRING_OBJECT_PATTERN = Pattern.compile("^\\{.*\\}$");
@Reference
private ConfigurationManager configManager;
@Reference
private ConfigurationPersistenceStrategyMultiplexer configurationPersistenceStrategy;
@Reference
private EditorConfig editorConfig;
@Reference
private DropdownOptionProviderService dropdownOptionProviderService;
private static Logger log = LoggerFactory.getLogger(ConfigDataServlet.class);
@Override
@SuppressWarnings("PMD.GuardLogStatement")
protected void doGet(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response) throws ServletException, IOException {
if (!editorConfig.isEnabled()) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
// get parameters
String configName = request.getParameter(RP_CONFIGNAME);
if (StringUtils.isBlank(configName)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
boolean collection = BooleanUtils.toBoolean(request.getParameter(RP_COLLECTION));
// output configuration
try {
Object result = getConfiguration(request.getResource(), configName, collection);
if (result == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
else {
response.setContentType("application/json;charset=" + StandardCharsets.UTF_8.name());
response.getWriter().write(OBJECT_MAPPER.writeValueAsString(result));
}
}
/*CHECKSTYLE:OFF*/ catch (Exception ex) { /*CHECKSTYLE:ON*/
log.error("Error getting configuration for " + configName + (collection ? "[col]" : ""), ex);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
}
}
private Object getConfiguration(@NotNull Resource contextResource, String configName, boolean collection) {
Object result;
if (collection) {
ConfigurationData newItem = configManager.newCollectionItem(contextResource, configName);
if (newItem == null) {
throw new ConfigurationPersistenceException("Invalid configuration name: " + configName);
}
result = fromConfigCollection(contextResource,
configManager.getConfigurationCollection(contextResource, configName), newItem, configName);
}
else {
ConfigurationData configData = configManager.getConfiguration(contextResource, configName);
if (configData != null) {
result = fromConfig(contextResource, configData, configData.isInherited(), configName);
}
else {
result = null;
}
}
return result;
}
private ConfigCollectionItem fromConfigCollection(@NotNull Resource contextResource,
ConfigurationCollectionData configCollection, ConfigurationData newItem, String fullConfigName) {
ConfigCollectionItem result = new ConfigCollectionItem();
result.setConfigName(configCollection.getConfigName());
if (!configCollection.getProperties().isEmpty()) {
Map<String, Object> properties = new TreeMap<>();
for (Map.Entry<String, Object> entry : configCollection.getProperties().entrySet()) {
properties.put(entry.getKey(), entry.getValue());
}
result.setProperties(properties);
}
List<ConfigItem> items = new ArrayList<>();
for (ConfigurationData configData : configCollection.getItems()) {
items.add(fromConfig(contextResource, configData, configData.isInherited(), fullConfigName));
}
result.setItems(items);
result.setNewItem(fromConfig(contextResource, newItem, null, fullConfigName));
return result;
}
private ConfigItem fromConfig(@NotNull Resource contextResource, ConfigurationData config, Boolean inherited, String fullConfigName) {
ConfigItem result = new ConfigItem();
result.setConfigName(config.getConfigName());
result.setCollectionItemName(config.getCollectionItemName());
result.setOverridden(config.isOverridden());
result.setInherited(inherited);
List<PropertyItem> props = new ArrayList<>();
for (String propertyName : config.getPropertyNames()) {
ValueInfo<?> item = config.getValueInfo(propertyName);
if (item == null) {
continue;
}
PropertyMetadata<?> itemMetadata = item.getPropertyMetadata();
PropertyItem prop = new PropertyItem();
prop.setName(item.getName());
// special handling for nested configs and nested config collections
if (itemMetadata != null && itemMetadata.isNestedConfiguration()) {
PropertyItemMetadata metadata = new PropertyItemMetadata();
metadata.setLabel(itemMetadata.getLabel());
metadata.setDescription(itemMetadata.getDescription());
metadata.setProperties(toJsonWithValueConversion(itemMetadata.getProperties(), contextResource));
prop.setMetadata(metadata);
if (itemMetadata.getType().isArray()) {
ConfigurationData[] configDatas = (ConfigurationData[])item.getValue();
if (configDatas != null) {
ConfigCollectionItem nestedConfigCollection = new ConfigCollectionItem();
StringBuilder collectionConfigName = new StringBuilder();
if (config.getCollectionItemName() != null) {
collectionConfigName.append(configurationPersistenceStrategy.getCollectionItemConfigName(fullConfigName
+ "/" + config.getCollectionItemName(), config.getResourcePath()));
}
else {
collectionConfigName.append(configurationPersistenceStrategy.getConfigName(fullConfigName, config.getResourcePath()));
}
collectionConfigName.append("/").append(itemMetadata.getConfigurationMetadata().getName());
nestedConfigCollection.setConfigName(collectionConfigName.toString());
List<ConfigItem> items = new ArrayList<>();
for (ConfigurationData configData : configDatas) {
items.add(fromConfig(contextResource, configData, false, collectionConfigName.toString()));
}
nestedConfigCollection.setItems(items);
prop.setNestedConfigCollection(nestedConfigCollection);
}
}
else {
ConfigurationData configData = (ConfigurationData)item.getValue();
if (configData != null) {
prop.setNestedConfig(fromConfig(contextResource, configData, null, fullConfigName
+ "/" + itemMetadata.getConfigurationMetadata().getName()));
}
}
}
// property data and metadata
else {
prop.setValue(item.getValue());
prop.setEffectiveValue(item.getEffectiveValue());
prop.setConfigSourcePath(item.getConfigSourcePath());
prop.setIsDefault(item.isDefault());
prop.setInherited(item.isInherited());
prop.setOverridden(item.isOverridden());
if (itemMetadata != null) {
PropertyItemMetadata metadata = new PropertyItemMetadata();
if (itemMetadata.getType().isArray()) {
metadata.setType(ClassUtils.primitiveToWrapper(itemMetadata.getType().getComponentType()).getSimpleName());
metadata.setMultivalue(true);
}
else {
metadata.setType(ClassUtils.primitiveToWrapper(itemMetadata.getType()).getSimpleName());
}
metadata.setDefaultValue(itemMetadata.getDefaultValue());
metadata.setLabel(itemMetadata.getLabel());
metadata.setDescription(itemMetadata.getDescription());
metadata.setProperties(toJsonWithValueConversion(itemMetadata.getProperties(), contextResource));
prop.setMetadata(metadata);
}
}
props.add(prop);
}
result.setProperties(props);
return result;
}
/**
* Converts the given map to JSON. Each map value is checked for a valid JSON string - if this is the case it's
* inserted as JSON objects and not as string.
* @param properties Map
* @param contextResource Context resource
* @return JSON object
*/
private @Nullable Map<String, Object> toJsonWithValueConversion(@Nullable Map<String, String> properties,
@NotNull Resource contextResource) {
if (properties == null || properties.isEmpty()) {
return null;
}
Map<String, Object> metadataProps = new TreeMap<>();
for (Map.Entry<String, String> entry : properties.entrySet()) {
metadataProps.put(entry.getKey(), tryConvertJsonString(entry.getValue()));
}
// check for dynamic dropdown option injection
boolean isDropdown = WIDGET_TYPE_DROPDOWN.equals(metadataProps.get(PROPERTY_WIDGET_TYPE));
if (isDropdown) {
Optional<String> dynamicProvider = Optional.ofNullable(metadataProps.get(PROPERTY_DROPDOWN_OPTIONS_PROVIDER))
.filter(Objects::nonNull)
.map(String::valueOf)
.filter(StringUtils::isNotBlank);
if (dynamicProvider.isPresent()) {
List<Map<String, Object>> items = dropdownOptionProviderService.getDropdownOptions(dynamicProvider.get(), contextResource);
if (!items.isEmpty()) {
metadataProps.put(PROPERTY_DROPDOWN_OPTIONS, items);
}
metadataProps.remove(PROPERTY_DROPDOWN_OPTIONS_PROVIDER);
}
}
return metadataProps;
}
private @Nullable Object tryConvertJsonString(@Nullable String value) {
if (value == null) {
return null;
}
if (JSON_STRING_ARRAY_PATTERN.matcher(value).matches()) {
try {
return OBJECT_MAPPER.readValue(value, List.class);
}
catch (IOException ex) {
// no valid json - ignore
log.trace("Conversion to JSON arary value failed for: {}", value, ex);
}
}
if (JSON_STRING_OBJECT_PATTERN.matcher(value).matches()) {
try {
return OBJECT_MAPPER.readValue(value, Map.class);
}
catch (IOException ex) {
// no valid json - ignore
log.trace("Conversion to JSON object value failed for: {}", value, ex);
}
}
return value;
}
}