| /* |
| * 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.feature.extension.apiregions.api.config.validation; |
| |
| import java.lang.reflect.Array; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.sling.feature.ArtifactId; |
| import org.apache.sling.feature.Configuration; |
| import org.apache.sling.feature.Feature; |
| import org.apache.sling.feature.builder.FeatureProvider; |
| import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationApi; |
| import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationDescription; |
| import org.apache.sling.feature.extension.apiregions.api.config.FactoryConfigurationDescription; |
| import org.apache.sling.feature.extension.apiregions.api.config.FrameworkPropertyDescription; |
| import org.apache.sling.feature.extension.apiregions.api.config.Mode; |
| import org.apache.sling.feature.extension.apiregions.api.config.Operation; |
| import org.apache.sling.feature.extension.apiregions.api.config.Region; |
| import org.osgi.util.converter.Converters; |
| |
| /** |
| * Validator to validate a feature |
| */ |
| public class FeatureValidator { |
| |
| private final ConfigurationValidator configurationValidator = new ConfigurationValidator(); |
| |
| private final PropertyValidator propertyValidator = new PropertyValidator(); |
| |
| private FeatureProvider featureProvider; |
| |
| private boolean liveValues = false; |
| |
| final Map<ArtifactId, Region> cache = new HashMap<>(); |
| |
| /** |
| * Create a new feature validator |
| */ |
| public FeatureValidator() { |
| this.configurationValidator.setCache(cache); |
| } |
| |
| /** |
| * Get the current feature provider |
| * @return the feature provider or {@code null} |
| */ |
| public FeatureProvider getFeatureProvider() { |
| return featureProvider; |
| } |
| |
| /** |
| * Set the feature provider |
| * @param provider the feature provider to set |
| */ |
| public void setFeatureProvider(final FeatureProvider provider) { |
| this.featureProvider = provider; |
| } |
| |
| /** |
| * Are live values validated? |
| * @return {@code true} if live values are validated |
| * @since 1.4 |
| */ |
| public boolean isLiveValues() { |
| return liveValues; |
| } |
| |
| /** |
| * Set whether live values are validated. |
| * @param value Flag for validating live values |
| * @since 1.4 |
| */ |
| public void setLiveValues(final boolean value) { |
| this.liveValues = value; |
| this.configurationValidator.setLiveValues(value); |
| this.propertyValidator.setLiveValues(value); |
| } |
| |
| /** |
| * Validate the feature against its configuration API |
| * @param feature The feature |
| * @return A {@code FeatureValidationResult} |
| * @throws IllegalArgumentException If api is not available |
| * @since 1.1 |
| */ |
| public FeatureValidationResult validate(final Feature feature) { |
| return validate(feature, ConfigurationApi.getConfigurationApi(feature)); |
| } |
| |
| /** |
| * Validate the feature against the configuration API |
| * @param feature The feature |
| * @param api The configuration API |
| * @return A {@code FeatureValidationResult} |
| * @throws IllegalArgumentException If api is {@code null} |
| */ |
| public FeatureValidationResult validate(final Feature feature, final ConfigurationApi api) { |
| final FeatureValidationResult result = new FeatureValidationResult(); |
| if ( api == null ) { |
| throw new IllegalArgumentException(); |
| } |
| cache.putAll(api.getFeatureToRegionCache()); |
| cache.put(feature.getId(), api.detectRegion()); |
| |
| for(final Configuration config : feature.getConfigurations()) { |
| final RegionInfo regionInfo = getRegionInfo(feature, config, cache); |
| |
| if ( regionInfo == null ) { |
| final ConfigurationValidationResult cvr = new ConfigurationValidationResult(); |
| cvr.getErrors().add("Unable to properly validate configuration, region info cannot be determined"); |
| result.getConfigurationResults().put(config.getPid(), cvr); |
| } else { |
| if ( config.isFactoryConfiguration() ) { |
| final FactoryConfigurationDescription desc = api.getFactoryConfigurationDescriptions().get(config.getFactoryPid()); |
| if ( desc != null ) { |
| final Mode validationMode = desc.getMode() != null ? desc.getMode() : api.getMode(); |
| final ConfigurationValidationResult r = configurationValidator.validate(config, desc, regionInfo.region, api.getMode()); |
| result.getConfigurationResults().put(config.getPid(), r); |
| if ( regionInfo.region != Region.INTERNAL ) { |
| if ( desc.getOperations().isEmpty() ) { |
| ConfigurationValidator.setResult(r, validationMode, desc, "No operations allowed for " + |
| "factory configuration"); |
| } else { |
| if ( regionInfo.isUpdate && !desc.getOperations().contains(Operation.UPDATE)) { |
| ConfigurationValidator.setResult(r, validationMode, desc, "Updating of factory " + |
| "configuration is not allowed"); |
| } else if ( !regionInfo.isUpdate && !desc.getOperations().contains(Operation.CREATE)) { |
| ConfigurationValidator.setResult(r, validationMode, desc, "Creation of factory " + |
| "configuration is not allowed"); |
| } |
| } |
| if ( desc.getInternalNames().contains(config.getName())) { |
| ConfigurationValidator.setResult(r, validationMode, desc, "Factory configuration with " + |
| "name is not allowed"); |
| } |
| } |
| |
| } else if ( regionInfo.region != Region.INTERNAL && api.getInternalFactoryConfigurations().contains(config.getFactoryPid())) { |
| final ConfigurationValidationResult cvr = new ConfigurationValidationResult(); |
| ConfigurationValidator.setResult(cvr, api.getMode(), desc, "Factory configuration is not " + |
| "allowed"); |
| result.getConfigurationResults().put(config.getPid(), cvr); |
| } |
| } else { |
| final ConfigurationDescription desc = api.getConfigurationDescriptions().get(config.getPid()); |
| if ( desc != null ) { |
| final ConfigurationValidationResult r = configurationValidator.validate(config, desc, regionInfo.region, api.getMode()); |
| result.getConfigurationResults().put(config.getPid(), r); |
| } else if ( regionInfo.region!= Region.INTERNAL && api.getInternalConfigurations().contains(config.getPid())) { |
| final ConfigurationValidationResult cvr = new ConfigurationValidationResult(); |
| ConfigurationValidator.setResult(cvr, api.getMode(), desc, "Configuration is not allowed"); |
| result.getConfigurationResults().put(config.getPid(), cvr); |
| } |
| } |
| } |
| |
| // make sure a result exists |
| result.getConfigurationResults().computeIfAbsent(config.getPid(), id -> new ConfigurationValidationResult()); |
| } |
| |
| for(final String frameworkProperty : feature.getFrameworkProperties().keySet()) { |
| final RegionInfo regionInfo = getRegionInfo(feature, frameworkProperty, cache); |
| if ( regionInfo == null ) { |
| final PropertyValidationResult pvr = new PropertyValidationResult(); |
| pvr.getErrors().add("Unable to properly validate framework property, region info cannot be determined"); |
| result.getFrameworkPropertyResults().put(frameworkProperty, pvr); |
| } else { |
| final FrameworkPropertyDescription fpd = api.getFrameworkPropertyDescriptions().get(frameworkProperty); |
| if ( fpd != null ) { |
| final PropertyValidationResult pvr = propertyValidator.validate(feature.getFrameworkProperties().get(frameworkProperty), fpd, api.getMode()); |
| result.getFrameworkPropertyResults().put(frameworkProperty, pvr); |
| } else if ( regionInfo.region != Region.INTERNAL && api.getInternalFrameworkProperties().contains(frameworkProperty) ) { |
| final PropertyValidationResult pvr = new PropertyValidationResult(); |
| PropertyValidator.setResult(pvr, null, api.getMode(), null, "Framework property is not allowed"); |
| result.getFrameworkPropertyResults().put(frameworkProperty, pvr); |
| } |
| } |
| // make sure a result exists |
| result.getFrameworkPropertyResults().computeIfAbsent(frameworkProperty, id -> new PropertyValidationResult()); |
| } |
| |
| return result; |
| } |
| |
| /** |
| * Apply default values from the result of a validation run. |
| * Defaults should be applied, if configuration properties are invalid and the validation mode |
| * for such a properties is definitive. |
| * @param feature The feature containing the configurations |
| * @param result The result |
| * @return {@code true} if a default value has been applied (the feature has been changed) |
| * @since 1.2 |
| */ |
| public boolean applyDefaultValues(final Feature feature, final FeatureValidationResult result) { |
| boolean changed = false; |
| |
| for(final Map.Entry<String, ConfigurationValidationResult> entry : result.getConfigurationResults().entrySet()) { |
| if ( entry.getValue().isUseDefaultValue() ) { |
| final Configuration cfg = feature.getConfigurations().getConfiguration(entry.getKey()); |
| if ( cfg != null ) { |
| boolean hasPrivateProperty = false; |
| final List<String> keys = new ArrayList<>(Collections.list(cfg.getConfigurationProperties().keys())); |
| for(final String k : keys ) { |
| final PropertyValidationResult pvr = entry.getValue().getPropertyResults().get(k); |
| if ( pvr != null && pvr.isUseDefaultValue() ) { |
| cfg.getProperties().remove(k); |
| changed = true; |
| } else { |
| hasPrivateProperty = true; |
| } |
| } |
| if ( !hasPrivateProperty ) { |
| feature.getConfigurations().remove(cfg); |
| changed = true; |
| } |
| } |
| } |
| for(final Map.Entry<String, PropertyValidationResult> propEntry : entry.getValue().getPropertyResults().entrySet()) { |
| if ( propEntry.getValue().isUseDefaultValue() ) { |
| final Configuration cfg = feature.getConfigurations().getConfiguration(entry.getKey()); |
| if ( cfg != null ) { |
| if ( propEntry.getValue().getDefaultValue() == null ) { |
| if ( propEntry.getValue().getUseExcludes() != null || propEntry.getValue().getUseIncludes() != null ) { |
| final List<String> includes = new ArrayList<>(); |
| final Set<String> excludes = new LinkedHashSet<>(); |
| if ( propEntry.getValue().getUseIncludes() != null ) { |
| for(final String v : propEntry.getValue().getUseIncludes()) { |
| includes.add(0, v); |
| } |
| } |
| if ( propEntry.getValue().getUseExcludes() != null ) { |
| for(final String v : propEntry.getValue().getUseExcludes()) { |
| excludes.add(v); |
| } |
| } |
| |
| Object value = cfg.getProperties().get(propEntry.getKey()); |
| if ( value.getClass().isArray() ) { |
| // array |
| int l = Array.getLength(value); |
| int i = 0; |
| while ( i < l ) { |
| final String val = Array.get(value, i).toString(); |
| if ( excludes.contains(val) ) { |
| final Object newArray = Array.newInstance(value.getClass().getComponentType(), l - 1); |
| int newIndex = 0; |
| for(int oldIndex = 0; oldIndex < l; oldIndex++) { |
| if ( oldIndex != i ) { |
| Array.set(newArray, newIndex, Array.get(value, oldIndex)); |
| newIndex++; |
| } |
| } |
| value = newArray; |
| i--; |
| l--; |
| changed = true; |
| cfg.getProperties().put(propEntry.getKey(), value); |
| } else if ( includes.contains(val) ) { |
| includes.remove(val); |
| } |
| i++; |
| } |
| for(final String val : includes) { |
| final Object newArray = Array.newInstance(value.getClass().getComponentType(), Array.getLength(value) + 1); |
| System.arraycopy(value, 0, newArray, 1, Array.getLength(value)); |
| Array.set(newArray, 0, |
| Converters.standardConverter().convert(val).to(value.getClass().getComponentType())); |
| value = newArray; |
| cfg.getProperties().put(propEntry.getKey(), value); |
| changed = true; |
| } |
| } else if ( value instanceof Collection ) { |
| // collection |
| final Collection c = (Collection)value; |
| final Class collectionType = c.isEmpty() ? String.class : c.iterator().next().getClass(); |
| final Iterator<?> i = c.iterator(); |
| while ( i.hasNext() ) { |
| final String val = i.next().toString(); |
| if ( excludes.contains(val) ) { |
| i.remove(); |
| changed = true; |
| } else if ( includes.contains(val) ) { |
| includes.remove(val); |
| } |
| } |
| for(final String val : includes) { |
| final Object newValue = Converters.standardConverter().convert(val).to(collectionType); |
| if ( c instanceof List ) { |
| ((List)c).add(0, newValue); |
| } else { |
| c.add(newValue); |
| } |
| changed = true; |
| } |
| } |
| } else { |
| cfg.getProperties().remove(propEntry.getKey()); |
| } |
| } else { |
| cfg.getProperties().put(propEntry.getKey(), propEntry.getValue().getDefaultValue()); |
| } |
| changed = true; |
| } |
| } |
| } |
| } |
| |
| for(final Map.Entry<String, PropertyValidationResult> propEntry : result.getFrameworkPropertyResults().entrySet()) { |
| if ( propEntry.getValue().isUseDefaultValue() ) { |
| if ( propEntry.getValue().getDefaultValue() == null ) { |
| feature.getFrameworkProperties().remove(propEntry.getKey()); |
| } else { |
| feature.getFrameworkProperties().put(propEntry.getKey(), propEntry.getValue().getDefaultValue().toString()); |
| } |
| changed = true; |
| } |
| } |
| |
| return changed; |
| } |
| |
| static Region getConfigurationApiRegion(final ArtifactId id, final Map<ArtifactId, Region> cache) { |
| Region result = cache.get(id); |
| if ( result == null ) { |
| result = Region.GLOBAL; |
| cache.put(id, result); |
| } |
| return result; |
| } |
| |
| static final class RegionInfo { |
| |
| public Region region; |
| |
| public boolean isUpdate; |
| } |
| |
| RegionInfo getRegionInfo(final Feature feature, final Configuration cfg, final Map<ArtifactId, Region> cache) { |
| final RegionInfo result = new RegionInfo(); |
| |
| final List<ArtifactId> list = cfg.getFeatureOrigins(); |
| if ( !list.isEmpty() ) { |
| boolean global = false; |
| for(final ArtifactId id : list) { |
| final Region region = getConfigurationApiRegion(id, cache); |
| if ( region == null ) { |
| return null; |
| } |
| if ( region == Region.GLOBAL ) { |
| global = true; |
| break; |
| } |
| } |
| result.region = global ? Region.GLOBAL : Region.INTERNAL; |
| result.isUpdate = list.size() > 1; |
| } else { |
| final Region region = getConfigurationApiRegion(feature.getId(), cache); |
| result.region = region == Region.INTERNAL ? Region.INTERNAL : Region.GLOBAL; |
| result.isUpdate = false; |
| } |
| return result; |
| } |
| |
| static Region getRegionInfo(final Region cfgRegion, final Configuration cfg, final String propertyName, final Map<ArtifactId, Region> cache) { |
| final List<ArtifactId> list = cfg.getFeatureOrigins(propertyName); |
| if ( !list.isEmpty() ) { |
| boolean global = false; |
| for(final ArtifactId id : list) { |
| final Region region = getConfigurationApiRegion(id, cache); |
| if ( region == null ) { |
| return null; |
| } |
| if ( region == Region.GLOBAL ) { |
| global = true; |
| break; |
| } |
| } |
| return global ? Region.GLOBAL : Region.INTERNAL; |
| } |
| return cfgRegion; |
| } |
| |
| RegionInfo getRegionInfo(final Feature feature, final String frameworkProperty, final Map<ArtifactId, Region> cache) { |
| final List<ArtifactId> list = feature.getFeatureOrigins(feature.getFrameworkPropertyMetadata(frameworkProperty)); |
| boolean global = false; |
| for(final ArtifactId id : list) { |
| final Region region = getConfigurationApiRegion(id, cache); |
| if ( region == null ) { |
| return null; |
| } |
| if ( region == Region.GLOBAL ) { |
| global = true; |
| break; |
| } |
| } |
| final RegionInfo result = new RegionInfo(); |
| result.region = global ? Region.GLOBAL : Region.INTERNAL; |
| result.isUpdate = list.size() > 1; |
| |
| return result; |
| } |
| } |