/* * 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.validation.impl; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.ResourceBundle; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import org.apache.commons.lang3.StringUtils; import org.apache.sling.api.resource.LoginException; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.api.resource.ValueMap; import org.apache.sling.api.wrappers.ValueMapDecorator; import org.apache.sling.i18n.ResourceBundleProvider; import org.apache.sling.serviceusermapping.ServiceUserMapped; import org.apache.sling.validation.SlingValidationException; import org.apache.sling.validation.ValidationResult; import org.apache.sling.validation.ValidationService; import org.apache.sling.validation.impl.ValidatorMap.ValidatorMetadata; import org.apache.sling.validation.model.ChildResource; import org.apache.sling.validation.model.ValidatorInvocation; import org.apache.sling.validation.model.ResourceProperty; import org.apache.sling.validation.model.ValidationModel; import org.apache.sling.validation.model.spi.ValidationModelRetriever; import org.apache.sling.validation.spi.ValidatorContext; import org.apache.sling.validation.spi.Validator; import org.osgi.framework.ServiceReference; 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.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; import org.osgi.service.component.annotations.ReferencePolicyOption; import org.osgi.service.metatype.annotations.Designate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component @Designate(ocd=ValidationServiceConfiguration.class) public class ValidationServiceImpl implements ValidationService{ /** Keys whose values are defined in the JCR resource bundle contained in the content-repository section of this bundle */ protected static final @Nonnull String I18N_KEY_WRONG_PROPERTY_TYPE = "sling.validator.wrong-property-type"; protected static final @Nonnull String I18N_KEY_EXPECTED_MULTIVALUE_PROPERTY = "sling.validator.multi-value-property-required"; protected static final @Nonnull String I18N_KEY_MISSING_REQUIRED_PROPERTY_WITH_NAME = "sling.validator.missing-required-property-with-name"; protected static final @Nonnull String I18N_KEY_MISSING_REQUIRED_PROPERTY_MATCHING_PATTERN = "sling.validator.missing-required-property-matching-pattern"; protected static final @Nonnull String I18N_KEY_MISSING_REQUIRED_CHILD_RESOURCE_WITH_NAME = "sling.validator.missing-required-child-resource-with-name"; protected static final @Nonnull String I18N_KEY_MISSING_REQUIRED_CHILD_RESOURCE_MATCHING_PATTERN = "sling.validator.missing-required-child-resource-matching-pattern"; private static final Logger LOG = LoggerFactory.getLogger(ValidationServiceImpl.class); @Reference ValidationModelRetriever modelRetriever; /** List of all known validators (key=id of validator) */ @Nonnull final ValidatorMap validatorMap; Collection<String> searchPaths; ValidationServiceConfiguration configuration; @Reference private ResourceResolverFactory rrf = null; /** * List of resource bundle providers, Declarative Services 1.3 takes care that the list is ordered according to {@link ServiceReference#compareTo(Object)}. * Highest ranked service is the last one in the list. * * @see OSGi R6 Comp, 112.3.8.1 */ @Reference(policy=ReferencePolicy.DYNAMIC, cardinality=ReferenceCardinality.AT_LEAST_ONE, policyOption=ReferencePolicyOption.GREEDY) volatile List<ResourceBundleProvider> resourceBundleProviders; @Reference private ServiceUserMapped serviceUserMapped; public ValidationServiceImpl() { this.validatorMap = new ValidatorMap(); } @Activate protected void activate(ValidationServiceConfiguration configuration) { this.configuration = configuration; ResourceResolver rr = null; try { rr = rrf.getServiceResourceResolver(null); searchPaths = Arrays.asList(rr.getSearchPath()); } catch (LoginException e) { throw new IllegalStateException("Could not get service resource resolver to figure out search paths", e); } finally { if (rr != null) { rr.close(); } } } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policyOption = ReferencePolicyOption.GREEDY, policy=ReferencePolicy.DYNAMIC) protected void addValidator(@Nonnull Validator<?> validator, Map<String, Object> properties, ServiceReference<Validator<?>> serviceReference) { validatorMap.put(properties, validator, serviceReference); } protected void removeValidator(@Nonnull Validator<?> validator, Map<String, Object> properties, ServiceReference<Validator<?>> serviceReference) { validatorMap.remove(properties, validator, serviceReference); } /** * Necessary to deal with property changes which do not lead to service restarts (when a modified method is provided) */ protected void updatedValidator(@Nonnull Validator<?> validator, Map<String, Object> properties, ServiceReference<Validator<?>> serviceReference) { validatorMap.update(properties, validator, serviceReference); } // ValidationService ################################################################################################################### public @CheckForNull ValidationModel getValidationModel(@Nonnull String validatedResourceType, String resourcePath, boolean considerResourceSuperTypeModels) { // https://bugs.eclipse.org/bugs/show_bug.cgi?id=459256 if (validatedResourceType == null) { throw new IllegalArgumentException("ValidationService.getValidationModel - cannot accept null as resource type. Resource path was: " + resourcePath); } // convert to relative resource types, see https://issues.apache.org/jira/browse/SLING-4262 validatedResourceType = getRelativeResourceType(validatedResourceType); return modelRetriever.getValidationModel(validatedResourceType, resourcePath, considerResourceSuperTypeModels); } /** * If the given resourceType is starting with a "/", it will strip out the leading search path from the given resource type. * Otherwise it will just return the given resource type (as this is already relative). * @param resourceType the resource type to convert * @return a relative resource type (without the leading search path) * @throws IllegalArgumentException in case the resource type is starting with a "/" but not with any of the search paths. */ protected @Nonnull String getRelativeResourceType(@Nonnull String resourceType) throws IllegalArgumentException { if (resourceType.startsWith("/")) { LOG.debug("try to strip the search path from the resource type"); for (String searchPath : searchPaths) { if (resourceType.startsWith(searchPath)) { resourceType = resourceType.substring(searchPath.length()); return resourceType; } } throw new IllegalArgumentException( "Can only deal with resource types inside the resource resolver's search path (" + StringUtils.join(searchPaths.toArray()) + ") but given resource type " + resourceType + " is outside!"); } return resourceType; } @Override public @CheckForNull ValidationModel getValidationModel(@Nonnull Resource resource, boolean considerResourceSuperTypeModels) { return getValidationModel(resource.getResourceType(), resource.getPath(), considerResourceSuperTypeModels); } @Override public @Nonnull ValidationResult validate(@Nonnull Resource resource, @Nonnull ValidationModel model) { return validate(resource, model, ""); } private @Nonnull ResourceBundle getDefaultResourceBundle() { Locale locale = Locale.ENGLISH; // go from highest ranked to lowest ranked providers for (int i = resourceBundleProviders.size() - 1; i >= 0; i--) { ResourceBundleProvider resourceBundleProvider = resourceBundleProviders.get(i); ResourceBundle defaultResourceBundle = resourceBundleProvider.getResourceBundle(locale); if (defaultResourceBundle != null) { return defaultResourceBundle; } } throw new IllegalStateException("There is no resource provider in the system, providing a resource bundle for locale"); } private int getSeverityForValidator(Integer severityFromModel, Integer severityFromValidator) { if (severityFromModel != null) { return severityFromModel; } if (severityFromValidator != null) { return severityFromValidator; } return configuration.defaultSeverity(); } protected @Nonnull ValidationResult validate(@Nonnull Resource resource, @Nonnull ValidationModel model, @Nonnull String relativePath) { if (resource == null || model == null || relativePath == null) { throw new IllegalArgumentException("ValidationService.validate - cannot accept null parameters"); } ResourceBundle defaultResourceBundle = getDefaultResourceBundle(); CompositeValidationResult result = new CompositeValidationResult(); ValueMap valueMap = resource.adaptTo(ValueMap.class); if (valueMap == null) { // SyntheticResources can not adapt to a ValueMap, therefore just use the empty map here valueMap = new ValueMapDecorator(Collections.emptyMap()); } // validate direct properties of the resource validateValueMap(valueMap, resource, relativePath, model.getResourceProperties(), result, defaultResourceBundle); // validate child resources, if any validateChildren(resource, relativePath, model.getChildren(), result, defaultResourceBundle); // optionally put result to cache if (configuration.cacheValidationResultsOnResources()) { ResourceToValidationResultAdapterFactory.putValidationResultToCache(result, resource); } return result; } /** * Validates a child resource with the help of the given {@code ChildResource} entry from the validation model * @param resource * @param relativePath relativePath of the resource (must be empty or end with "/") * @param result * @param childResources */ private void validateChildren(@Nonnull Resource resource, @Nonnull String relativePath, @Nonnull Collection<ChildResource> childResources, @Nonnull CompositeValidationResult result, @Nonnull ResourceBundle defaultResourceBundle) { // validate children resources, if any for (ChildResource childResource : childResources) { // if a pattern is set we validate all children matching that pattern Pattern pattern = childResource.getNamePattern(); if (pattern != null) { boolean foundMatch = false; for (Resource child : resource.getChildren()) { Matcher matcher = pattern.matcher(child.getName()); if (matcher.matches()) { validateChildResource(child, relativePath, childResource, result, defaultResourceBundle); foundMatch = true; } } if (!foundMatch && childResource.isRequired()) { result.addFailure(relativePath, configuration.defaultSeverity(), defaultResourceBundle, I18N_KEY_MISSING_REQUIRED_CHILD_RESOURCE_MATCHING_PATTERN, pattern.toString()); } } else { Resource expectedResource = resource.getChild(childResource.getName()); if (expectedResource != null) { validateChildResource(expectedResource, relativePath, childResource, result, defaultResourceBundle); } else if (childResource.isRequired()) { result.addFailure(relativePath, configuration.defaultSeverity(), defaultResourceBundle, I18N_KEY_MISSING_REQUIRED_CHILD_RESOURCE_WITH_NAME, childResource.getName()); } } } } private void validateChildResource(@Nonnull Resource resource, @Nonnull String relativePathOfParent, @Nonnull ChildResource childResource, @Nonnull CompositeValidationResult result, @Nonnull ResourceBundle defaultResourceBundle) { final @Nonnull String relativePath; if (relativePathOfParent.isEmpty()) { relativePath = resource.getName(); } else { relativePath = relativePathOfParent + "/" + resource.getName(); } validateValueMap(resource.adaptTo(ValueMap.class), resource, relativePath, childResource.getProperties(), result, defaultResourceBundle); validateChildren(resource, relativePath, childResource.getChildren(), result, defaultResourceBundle); } @Override public @Nonnull ValidationResult validate(@Nonnull ValueMap valueMap, @Nonnull ValidationModel model) { if (valueMap == null || model == null) { throw new IllegalArgumentException("ValidationResult.validate - cannot accept null parameters"); } ResourceBundle defaultResourceBundle = getDefaultResourceBundle(); CompositeValidationResult result = new CompositeValidationResult(); validateValueMap(valueMap, null, "", model.getResourceProperties(), result, defaultResourceBundle); return result; } @Override public @Nonnull ValidationResult validateResourceRecursively(@Nonnull Resource resource, boolean enforceValidation, Predicate<Resource> filter, boolean considerResourceSuperTypeModels) throws IllegalStateException, IllegalArgumentException, SlingValidationException { ValidationResourceVisitor visitor = new ValidationResourceVisitor(this, resource.getPath(), enforceValidation, filter, considerResourceSuperTypeModels); visitor.accept(resource); return visitor.getResult(); } private void validateValueMap(ValueMap valueMap, Resource resource, @Nonnull String relativePath, @Nonnull Collection<ResourceProperty> resourceProperties, @Nonnull CompositeValidationResult result, @Nonnull ResourceBundle defaultResourceBundle) { if (valueMap == null) { throw new IllegalArgumentException("ValueMap may not be null"); } for (ResourceProperty resourceProperty : resourceProperties) { Pattern pattern = resourceProperty.getNamePattern(); if (pattern != null) { boolean foundMatch = false; for (String key : valueMap.keySet()) { if (pattern.matcher(key).matches()) { foundMatch = true; validatePropertyValue(key, valueMap, resource, relativePath, resourceProperty, result, defaultResourceBundle); } } if (!foundMatch && resourceProperty.isRequired()) { result.addFailure(relativePath, configuration.defaultSeverity(), defaultResourceBundle, I18N_KEY_MISSING_REQUIRED_PROPERTY_MATCHING_PATTERN, pattern.toString()); } } else { validatePropertyValue(resourceProperty.getName(), valueMap, resource, relativePath, resourceProperty, result, defaultResourceBundle); } } } private void validatePropertyValue(@Nonnull String property, ValueMap valueMap, Resource resource, @Nonnull String relativePath, @Nonnull ResourceProperty resourceProperty, @Nonnull CompositeValidationResult result, @Nonnull ResourceBundle defaultResourceBundle) { Object fieldValues = valueMap.get(property); if (fieldValues == null) { if (resourceProperty.isRequired()) { result.addFailure(relativePath, configuration.defaultSeverity(), defaultResourceBundle, I18N_KEY_MISSING_REQUIRED_PROPERTY_WITH_NAME, property); } return; } List<ValidatorInvocation> validatorInvocations = resourceProperty.getValidatorInvocations(); if (resourceProperty.isMultiple()) { if (!fieldValues.getClass().isArray()) { result.addFailure(relativePath + property, configuration.defaultSeverity(), defaultResourceBundle, I18N_KEY_EXPECTED_MULTIVALUE_PROPERTY); return; } } for (ValidatorInvocation validatorInvocation : validatorInvocations) { // lookup validator by id ValidatorMetadata validatorMetadata = validatorMap.get(validatorInvocation.getValidatorId()); if (validatorMetadata == null) { throw new IllegalStateException("Could not find validator with id '" + validatorInvocation.getValidatorId() + "'"); } int severity = getSeverityForValidator(validatorInvocation.getSeverity(), validatorMetadata.getSeverity()); // convert the type always to an array Class<?> type = validatorMetadata.getType(); if (!type.isArray()) { try { // https://docs.oracle.com/javase/6/docs/api/java/lang/Class.html#getName%28%29 has some hints on class names type = Class.forName("[L"+type.getName()+";", false, type.getClassLoader()); } catch (ClassNotFoundException e) { throw new SlingValidationException("Could not generate array class for type " + type, e); } } // it is already validated here that the property exists in the value map Object[] typedValue = (Object[])valueMap.get(property, type); // see https://issues.apache.org/jira/browse/SLING-4178 for why the second check is necessary if (typedValue == null || (typedValue.length > 0 && typedValue[0] == null)) { // here the missing required property case was already treated in validateValueMap result.addFailure(relativePath + property, severity, defaultResourceBundle, I18N_KEY_WRONG_PROPERTY_TYPE, validatorMetadata.getType()); return; } // see https://issues.apache.org/jira/browse/SLING-662 for a description on how multivalue properties are treated with ValueMap if (validatorMetadata.getType().isArray()) { // ValueMap already returns an array in both cases (property is single value or multivalue) validateValue(result, typedValue, property, relativePath, valueMap, resource, validatorMetadata.getValidator(), validatorInvocation.getParameters(), defaultResourceBundle, severity); } else { // call validate for each entry in the array (supports both singlevalue and multivalue) @Nonnull Object[] array = (Object[])typedValue; if (array.length == 1) { validateValue(result, array[0], property, relativePath, valueMap, resource, validatorMetadata.getValidator(), validatorInvocation.getParameters(), defaultResourceBundle, severity); } else { int n = 0; for (Object item : array) { validateValue(result, item, property + "[" + n++ + "]", relativePath, valueMap, resource, validatorMetadata.getValidator(), validatorInvocation.getParameters(), defaultResourceBundle, severity); } } } } } @SuppressWarnings({ "rawtypes", "unchecked" }) private void validateValue(CompositeValidationResult result, @Nonnull Object value, String property, String relativePath, @Nonnull ValueMap valueMap, Resource resource, @Nonnull Validator validator, ValueMap validatorParameters, @Nonnull ResourceBundle defaultResourceBundle, int severity) { try { ValidatorContext validationContext = new ValidatorContextImpl(relativePath + property, severity, valueMap, resource, defaultResourceBundle); ValidationResult validatorResult = ((Validator)validator).validate(value, validationContext, validatorParameters); result.addValidationResult(validatorResult); } catch (SlingValidationException e) { // wrap in another SlingValidationException to include information about the property throw new SlingValidationException("Could not call validator " + validator .getClass().getName() + " for resourceProperty " + relativePath + property, e); } } }