org.apache.sling.validation.impl.ValidationServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.sling.validation.impl.ValidationServiceImpl.java

Source

/*
 * 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.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.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
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.ResourceUtil;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.apache.sling.validation.SlingValidationException;
import org.apache.sling.validation.ValidationResult;
import org.apache.sling.validation.ValidationService;
import org.apache.sling.validation.model.ChildResource;
import org.apache.sling.validation.model.ParameterizedValidator;
import org.apache.sling.validation.model.ResourceProperty;
import org.apache.sling.validation.model.ValidationModel;
import org.apache.sling.validation.spi.ValidationContext;
import org.apache.sling.validation.spi.Validator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Component()
@Service()
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 String I18N_KEY_WRONG_PROPERTY_TYPE = "sling.validator.wrong-property-type";
    protected static final String I18N_KEY_EXPECTED_MULTIVALUE_PROPERTY = "sling.validator.multi-value-property-required";
    protected static final String I18N_KEY_MISSING_REQUIRED_PROPERTY_WITH_NAME = "sling.validator.missing-required-property-with-name";
    protected static final String I18N_KEY_MISSING_REQUIRED_PROPERTY_MATCHING_PATTERN = "sling.validator.missing-required-property-matching-pattern";
    protected static final String I18N_KEY_MISSING_REQUIRED_CHILD_RESOURCE_WITH_NAME = "sling.validator.missing-required-child-resource-with-name";
    protected static final 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;

    Collection<String> searchPaths;

    @Reference
    private ResourceResolverFactory rrf = null;

    @Activate
    public void activate() {
        ResourceResolver rr = null;
        try {
            rr = rrf.getAdministrativeResourceResolver(null);
            searchPaths = Arrays.asList(rr.getSearchPath());
        } catch (LoginException e) {
            throw new IllegalStateException("Could not login as administrator to figure out search paths", e);
        } finally {
            if (rr != null) {
                rr.close();
            }
        }
    }

    // ValidationService ###################################################################################################################

    @SuppressWarnings("unused")
    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.getModel(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
     * @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.
     */
    @SuppressWarnings("null")
    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;
    }

    @SuppressWarnings("null")
    @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, "");
    }

    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");
        }
        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);

        // validate child resources, if any
        validateChildren(resource, relativePath, model.getChildren(), result);
        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(Resource resource, String relativePath, Collection<ChildResource> childResources,
            CompositeValidationResult result) {
        // 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);
                        foundMatch = true;
                    }
                }
                if (!foundMatch && childResource.isRequired()) {
                    result.addFailure(relativePath, null, 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);
                } else if (childResource.isRequired()) {
                    result.addFailure(relativePath, null, I18N_KEY_MISSING_REQUIRED_CHILD_RESOURCE_WITH_NAME,
                            childResource.getName());
                }
            }
        }
    }

    private void validateChildResource(Resource resource, String relativePathOfParent, ChildResource childResource,
            CompositeValidationResult result) {
        final String relativePath;
        if (relativePathOfParent.isEmpty()) {
            relativePath = resource.getName();
        } else {
            relativePath = relativePathOfParent + "/" + resource.getName();
        }
        validateValueMap(resource.adaptTo(ValueMap.class), resource, relativePath, childResource.getProperties(),
                result);
        validateChildren(resource, relativePath, childResource.getChildren(), result);
    }

    @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");
        }
        CompositeValidationResult result = new CompositeValidationResult();
        validateValueMap(valueMap, null, "", model.getResourceProperties(), result);
        return result;
    }

    @Override
    public @Nonnull ValidationResult validateResourceRecursively(@Nonnull Resource resource,
            boolean enforceValidation, Predicate 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, String relativePath,
            Collection<ResourceProperty> resourceProperties, CompositeValidationResult result) {
        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);
                    }
                }
                if (!foundMatch && resourceProperty.isRequired()) {
                    result.addFailure(relativePath, null, I18N_KEY_MISSING_REQUIRED_PROPERTY_MATCHING_PATTERN,
                            pattern.toString());
                }
            } else {
                validatePropertyValue(resourceProperty.getName(), valueMap, resource, relativePath,
                        resourceProperty, result);
            }
        }
    }

    private void validatePropertyValue(String property, ValueMap valueMap, Resource resource, String relativePath,
            ResourceProperty resourceProperty, CompositeValidationResult result) {
        Object fieldValues = valueMap.get(property);
        if (fieldValues == null) {
            if (resourceProperty.isRequired()) {
                result.addFailure(relativePath, null, I18N_KEY_MISSING_REQUIRED_PROPERTY_WITH_NAME, property);
            }
            return;
        }
        List<ParameterizedValidator> validators = resourceProperty.getValidators();
        if (resourceProperty.isMultiple()) {
            if (!fieldValues.getClass().isArray()) {
                result.addFailure(relativePath + property, null, I18N_KEY_EXPECTED_MULTIVALUE_PROPERTY);
                return;
            }
        }

        for (ParameterizedValidator validator : validators) {
            // convert the type always to an array
            Class<?> type = validator.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, validator.getSeverity(), I18N_KEY_WRONG_PROPERTY_TYPE,
                        validator.getType());
                return;
            }

            // see https://issues.apache.org/jira/browse/SLING-662 for a description on how multivalue properties are treated with ValueMap
            if (validator.getType().isArray()) {
                // ValueMap already returns an array in both cases (property is single value or multivalue)
                validateValue(result, typedValue, property, relativePath, valueMap, resource, validator);
            } 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, validator);
                } else {
                    int n = 0;
                    for (Object item : array) {
                        validateValue(result, item, property + "[" + n++ + "]", relativePath, valueMap, resource,
                                validator);
                    }
                }
            }
        }
    }

    @SuppressWarnings("rawtypes")
    private void validateValue(CompositeValidationResult result, @Nonnull Object value, String property,
            String relativePath, @Nonnull ValueMap valueMap, Resource resource, ParameterizedValidator validator) {
        try {
            @SuppressWarnings("unchecked")
            ValidationContext validationContext = new ValidationContextImpl(relativePath + property,
                    validator.getSeverity(), valueMap, resource);
            ValidationResult validatorResult = ((Validator) validator.getValidator()).validate(value,
                    validationContext, validator.getParameters());
            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);
        }
    }

}