All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.jackrabbit.vault.validation.ValidationExecutor Maven / Gradle / Ivy

Go to download

Provides a validation framework with SPI and API for performing validations on FileVault packages

There is a newer version: 3.8.4
Show newest version
/*
 * 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.jackrabbit.vault.validation;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.io.FilenameUtils;
import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.vault.util.Constants;
import org.apache.jackrabbit.vault.util.PlatformNameFormat;
import org.apache.jackrabbit.vault.validation.impl.util.EnhancedBufferedInputStream;
import org.apache.jackrabbit.vault.validation.impl.util.ResettableInputStream;
import org.apache.jackrabbit.vault.validation.impl.util.ValidatorException;
import org.apache.jackrabbit.vault.validation.spi.DocumentViewXmlValidator;
import org.apache.jackrabbit.vault.validation.spi.FilterValidator;
import org.apache.jackrabbit.vault.validation.spi.GenericJcrDataValidator;
import org.apache.jackrabbit.vault.validation.spi.GenericMetaInfDataValidator;
import org.apache.jackrabbit.vault.validation.spi.JcrPathValidator;
import org.apache.jackrabbit.vault.validation.spi.MetaInfPathValidator;
import org.apache.jackrabbit.vault.validation.spi.NodeContext;
import org.apache.jackrabbit.vault.validation.spi.NodePathValidator;
import org.apache.jackrabbit.vault.validation.spi.OsgiConfigurationValidator;
import org.apache.jackrabbit.vault.validation.spi.PropertiesValidator;
import org.apache.jackrabbit.vault.validation.spi.ValidationContext;
import org.apache.jackrabbit.vault.validation.spi.ValidationMessage;
import org.apache.jackrabbit.vault.validation.spi.ValidationMessageSeverity;
import org.apache.jackrabbit.vault.validation.spi.Validator;
import org.apache.jackrabbit.vault.validation.spi.impl.AdvancedFilterValidator;
import org.apache.jackrabbit.vault.validation.spi.impl.AdvancedPropertiesValidator;
import org.apache.jackrabbit.vault.validation.spi.impl.DocumentViewParserValidator;
import org.apache.jackrabbit.vault.validation.spi.impl.DocumentViewParserValidatorFactory;
import org.apache.jackrabbit.vault.validation.spi.impl.OsgiConfigurationParserValidator;
import org.apache.jackrabbit.vault.validation.spi.impl.OsgiConfigurationParserValidatorFactory;
import org.apache.jackrabbit.vault.validation.spi.util.NodeContextImpl;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** 
 * Provides methods to call all registered validators. This instance is bound to the {@link ValidationContext} being given in the 
 * {@link ValidationExecutorFactory#createValidationExecutor(org.apache.jackrabbit.vault.validation.spi.ValidationContext, boolean, boolean, Map)}.
 * This class is thread-safe (i.e. methods can be used from different threads on the same instance). 
 * @see ValidationExecutorFactory
 */
public final class ValidationExecutor {

    public static final String EXTENSION_BINARY = ".binary";
    private final Map<@NotNull String, @NotNull DocumentViewXmlValidator> documentViewXmlValidators;
    private final Map<@NotNull String, @NotNull NodePathValidator> nodePathValidators;
    private final Map<@NotNull String, @NotNull GenericJcrDataValidator> genericJcrDataValidators;
    private final Map<@NotNull String, @NotNull GenericMetaInfDataValidator> genericMetaInfDataValidators;
    private final Map<@NotNull String, @NotNull MetaInfPathValidator> metaInfPathValidators;
    private final Map<@NotNull String, @NotNull JcrPathValidator> jcrPathValidators;
    private final Map<@NotNull String, @NotNull FilterValidator> filterValidators;
    private final Map<@NotNull String, @NotNull PropertiesValidator> propertiesValidators;
    private final @NotNull Map<@NotNull String, @NotNull Validator> validatorsById;
    private final Map<@NotNull String, @NotNull OsgiConfigurationValidator> osgiConfigurationValidators;

    /**
     * the default logger
     */
    private static final Logger log = LoggerFactory.getLogger(ValidationExecutor.class);

    /**
     * Creates a new instance with the given validators.
     * 
     * @param validatorsById a map of validator ids and actual validators
     */
    public ValidationExecutor(@NotNull Map<@NotNull String, @NotNull Validator> validatorsById) {
        this.validatorsById = new LinkedHashMap<>(validatorsById);
        this.osgiConfigurationValidators = ValidationExecutor.filterValidatorsByClass(this.validatorsById, OsgiConfigurationValidator.class);
        if (osgiConfigurationValidators.isEmpty()) {
            // remove irrelevant parser in case no downstream validators are registered
            this.validatorsById.remove(OsgiConfigurationParserValidatorFactory.ID);
        }
        this.documentViewXmlValidators = ValidationExecutor.filterValidatorsByClass(this.validatorsById, DocumentViewXmlValidator.class);
        this.nodePathValidators = ValidationExecutor.filterValidatorsByClass(this.validatorsById, NodePathValidator.class);
        this.genericJcrDataValidators = ValidationExecutor.filterValidatorsByClass(this.validatorsById, GenericJcrDataValidator.class);
        this.genericMetaInfDataValidators = ValidationExecutor.filterValidatorsByClass(this.validatorsById, GenericMetaInfDataValidator.class);
        this.metaInfPathValidators = ValidationExecutor.filterValidatorsByClass(this.validatorsById, MetaInfPathValidator.class);
        this.jcrPathValidators = ValidationExecutor.filterValidatorsByClass(this.validatorsById, JcrPathValidator.class);
        this.filterValidators = ValidationExecutor.filterValidatorsByClass(this.validatorsById, FilterValidator.class);
        this.propertiesValidators = ValidationExecutor.filterValidatorsByClass(this.validatorsById, PropertiesValidator.class);
        // nested validators (i.e. ones called from specific low-level validators) need to be linked
        for (Validator validator : validatorsById.values()) {
            if (validator instanceof AdvancedFilterValidator) {
                AdvancedFilterValidator.class.cast(validator).setFilterValidators(filterValidators);
            }
            if (validator instanceof AdvancedPropertiesValidator) {
                AdvancedPropertiesValidator.class.cast(validator).setPropertiesValidators(propertiesValidators);
            }
            if (validator instanceof DocumentViewParserValidator) {
                DocumentViewParserValidator.class.cast(validator).setDocumentViewXmlValidators(documentViewXmlValidators);
            }
            if (validator instanceof OsgiConfigurationParserValidator) {
                OsgiConfigurationParserValidator.class.cast(validator).setOsgiConfigurationValidators(osgiConfigurationValidators);
            }
        }
        
        // jcr path validators requires documentviewparservalidator (as that retrieves the JCR paths in the first place)
        if (!(jcrPathValidators.isEmpty() || nodePathValidators.isEmpty()) && !genericJcrDataValidators.containsKey(DocumentViewParserValidatorFactory.ID)) {
            Set pathValidatorIds = new HashSet<>(jcrPathValidators.keySet());
            pathValidatorIds.addAll(nodePathValidators.keySet());
            throw new IllegalStateException("For the validators with id(s) '" + String.join(", ", pathValidatorIds) + "' it is mandatory to also enable validator '" + DocumentViewParserValidatorFactory.ID + "'");
        }
        
    }

    /**
     * Returns all bound validators by id.
     * @return a map with all validators (key=validator id, value=actual validator)
     */
    public @NotNull Map getAllValidatorsById() {
        return validatorsById;
    }

    /**
     * Returns all unused validators by id. Unused validators are those implementing an interface which
     * is not understood by this executor.
     * 
     * @return a map with all unused validators (key=validator id, value=actual validator)
     */
    public @NotNull Map getUnusedValidatorsById() {
        Map unusedValidators = new HashMap<>(validatorsById);
        unusedValidators.keySet().removeAll(documentViewXmlValidators.keySet());
        unusedValidators.keySet().removeAll(nodePathValidators.keySet());
        unusedValidators.keySet().removeAll(metaInfPathValidators.keySet());
        unusedValidators.keySet().removeAll(jcrPathValidators.keySet());
        unusedValidators.keySet().removeAll(genericJcrDataValidators.keySet());
        unusedValidators.keySet().removeAll(genericMetaInfDataValidators.keySet());
        // plus the ones bound to other validators
        unusedValidators.keySet().removeAll(filterValidators.keySet());
        unusedValidators.keySet().removeAll(propertiesValidators.keySet());
        unusedValidators.keySet().removeAll(osgiConfigurationValidators.keySet());
        return unusedValidators;
    }

    /** 
     * Validates a package's META-INF input stream  with all relevant validators.
     * 
     * @param input the input stream if it is a file or {@code null} in case it is called for a folder. It is not closed during processing, this is obligation of the caller. Should not be buffered as buffering is done internally! 
     * @param filePath should be relative to the META-INF directory (i.e. should not start with {@code META-INF})
     * @param basePath the path to which the file path is relative
     * @return the list of validation messages 
     * @throws IOException in case the input stream could not be accessed */
    public @NotNull Collection validateMetaInf(@Nullable InputStream input, @NotNull Path filePath, @NotNull Path basePath) throws IOException {
        if (filePath.isAbsolute()) {
            throw new IllegalArgumentException("Given file path must not be absolute");
        }
        if (filePath.startsWith(Constants.META_INF)) {
            throw new IllegalArgumentException("Given file path must not start with META-INF but rather on the level below");
        }
        List messages = new LinkedList<>();
        messages.add(new ValidationViolation(ValidationMessageSeverity.DEBUG, "Validating meta inf file '" + filePath + "'..."));
        messages.addAll(validateGenericMetaInfData(input != null ? new EnhancedBufferedInputStream(input) : null, filePath, basePath));
        return messages;
    }

    /** 
     * Validates a package's input stream (below jcr_root, no metadata) with all relevant validators.
     * 

* As some validators rely on the order of validated files, make sure to first call for streams representing the parent nodes (i.e. folders and .content.xml streams), before calling it for streams representing the children. * * @param input the input stream if it is a file or {@code null} in case it is called for a folder. It is not closed during processing, this is obligation of the caller. Should not be buffered as buffering is done internally! * @param filePath file path relative to the content package jcr root (i.e. the folder named "jcr_root") * @param basePath the path to which the file path is relative * @return the list of validation messages * @throws IOException in case the input stream could not be accessed */ public @NotNull Collection validateJcrRoot(@Nullable InputStream input, @NotNull Path filePath, @NotNull Path basePath) throws IOException { if (filePath.isAbsolute()) { throw new IllegalArgumentException("Given path is not relative " + filePath); } if (filePath.startsWith(Constants.ROOT_DIR)) { throw new IllegalArgumentException("Given file path must not start with jcr_root but rather on the level below"); } List messages = new LinkedList<>(); messages.add(new ValidationViolation(ValidationMessageSeverity.DEBUG, "Validating jcr file '" + filePath + "'...")); messages.addAll(validateGenericJcrData(input != null ? new EnhancedBufferedInputStream(input) : null, filePath, basePath)); return messages; } /** * Must be called at the end of the validation (when the validation context is no longer used). * This is important as some validators emit violation messages only when this method is called. * * @return the list of additional validation violations (might be empty) which have not been reported before */ public @NotNull Collection done() { Collection allViolations = new LinkedList<>(); // go through all validators (even the nested ones) for (Map.Entry<@NotNull String, @NotNull Validator>entry : validatorsById.entrySet()) { try { Collection violations = entry.getValue().done(); if (violations != null && !violations.isEmpty()) { allViolations.addAll(ValidationViolation.wrapMessages(entry.getKey(), violations, null, null, null, 0, 0)); } } catch (RuntimeException e) { throw new ValidatorException(entry.getKey(), e); } } return allViolations; } private Collection validateNodePaths(Path filePath, Path basePath, Map nodePathsAndLineNumbers) { List enrichedMessages = new LinkedList<>(); for (Map.Entry nodePathAndLineNumber : nodePathsAndLineNumbers.entrySet()) { for (Map.Entry entry : nodePathValidators.entrySet()) { enrichedMessages.add(new ValidationViolation(entry.getKey(), ValidationMessageSeverity.DEBUG, "Validate...")); try { Collection messages = entry.getValue().validate(new NodeContextImpl(nodePathAndLineNumber.getKey(), filePath, basePath)); if (messages != null && !messages.isEmpty()) { enrichedMessages.addAll(ValidationViolation.wrapMessages(entry.getKey(), messages, filePath, basePath, nodePathAndLineNumber.getKey(), nodePathAndLineNumber.getValue().intValue(), 0)); } } catch (RuntimeException e) { throw new ValidatorException(entry.getKey(), nodePathAndLineNumber.getKey(), filePath, e); } } } return enrichedMessages; } private Collection validateGenericMetaInfData(@Nullable InputStream input, @NotNull Path filePath, @NotNull Path basePath) throws IOException { Collection enrichedMessages = new LinkedList<>(); for (Map.Entry entry : metaInfPathValidators.entrySet()) { Collection messages = entry.getValue().validateMetaInfPath(filePath, basePath, input == null); if (messages != null && !messages.isEmpty()) { enrichedMessages.addAll(ValidationViolation.wrapMessages(entry.getKey(), messages, filePath, basePath, null, 0, 0)); } } if (input != null) { InputStream currentInput = input; ResettableInputStream resettableInputStream = null; try { for (Map.Entry entry : genericMetaInfDataValidators.entrySet()) { try { GenericMetaInfDataValidator validator = entry.getValue(); if (validator.shouldValidateMetaInfData(filePath, basePath)) { if (resettableInputStream == null) { boolean isAnotherValidatorInterested = genericMetaInfDataValidators.values().stream().filter(t-> !t.equals(validator)).anyMatch(x -> x.shouldValidateMetaInfData(filePath, basePath)); if (isAnotherValidatorInterested) { currentInput = resettableInputStream = new ResettableInputStream(input); } } else { resettableInputStream.reset(); } enrichedMessages.add(new ValidationViolation(entry.getKey(), ValidationMessageSeverity.DEBUG, "Validate...")); Collection messages = validator.validateMetaInfData(currentInput, filePath, basePath); if (messages != null && !messages.isEmpty()) { enrichedMessages.addAll(ValidationViolation.wrapMessages(entry.getKey(), messages, filePath, basePath, null, 0, 0)); } } } catch (RuntimeException e) { if (!(e instanceof ValidatorException)) { throw new ValidatorException(entry.getKey(), filePath, e); } } } } finally { if (resettableInputStream != null) { resettableInputStream.close(); } } } return enrichedMessages; } private Collection validateGenericJcrData(@Nullable InputStream input, @NotNull Path filePath, @NotNull Path basePath) throws IOException { Map nodePathsAndLineNumbers = new LinkedHashMap<>(); Collection enrichedMessages = new LinkedList<>(); boolean isDocViewXml = true; if (input != null) { InputStream currentInput = input; ResettableInputStream resettableInputStream = null; try { // make sure the docviewparser always comes first for (Map.Entry entry : genericJcrDataValidators.entrySet()) { try { GenericJcrDataValidator validator = entry.getValue(); log.debug("Validate {} with validator '{}'", filePath, validator.getClass().getName()); if (validator.shouldValidateJcrData(filePath, basePath)) { if (resettableInputStream == null) { boolean isAnotherValidatorInterested = genericJcrDataValidators.values().stream().filter(t-> !t.equals(validator)).anyMatch(x -> x.shouldValidateJcrData(filePath, basePath)); if (isAnotherValidatorInterested) { currentInput = resettableInputStream = new ResettableInputStream(input); } } else { resettableInputStream.reset(); } enrichedMessages.add(new ValidationViolation(entry.getKey(), ValidationMessageSeverity.DEBUG, "Validate...")); Collection messages = validator.validateJcrData(currentInput, filePath, basePath, nodePathsAndLineNumbers); if (messages != null && !messages.isEmpty()) { enrichedMessages.addAll(ValidationViolation.wrapMessages(entry.getKey(), messages, filePath, basePath, null, 0, 0)); } } // if we haven't collected node paths from a previous run the input is no docview xml if (nodePathsAndLineNumbers.isEmpty()) { // convert file name to node path String nodePath = filePathToNodePath(filePath); log.debug("Found non-docview node '{}'", nodePath); isDocViewXml = false; nodePathsAndLineNumbers.put(nodePath, 0); } } catch (RuntimeException e) { if (!(e instanceof ValidatorException)) { throw new ValidatorException(entry.getKey(), filePath, e); } else { throw e; } } } } finally { if (resettableInputStream != null) { resettableInputStream.close(); } } } else { // collect node path for folder only nodePathsAndLineNumbers.put(filePathToNodePath(filePath), 0); } if (!jcrPathValidators.isEmpty() || !nodePathValidators.isEmpty()) { NodeContext nodeContext = new NodeContextImpl(nodePathsAndLineNumbers.keySet().stream().findFirst().orElseThrow(() -> new IllegalStateException("No node paths have been collected")), filePath, basePath); for (Map.Entry entry : jcrPathValidators.entrySet()) { Collection messages = entry.getValue().validateJcrPath(nodeContext, input == null, isDocViewXml); if (messages != null && !messages.isEmpty()) { enrichedMessages.addAll(ValidationViolation.wrapMessages(entry.getKey(), messages, filePath, basePath, null, 0, 0)); } } enrichedMessages.addAll(validateNodePaths(filePath, basePath, nodePathsAndLineNumbers)); } return enrichedMessages; } /** * Converts the given file path (a relative one) to the absolute node path. * @param filePath the relative file path to convert * @return the node path */ public static @NotNull String filePathToNodePath(@NotNull Path filePath) { String platformPath = FilenameUtils.separatorsToUnix(filePath.toString()); // treat https://jackrabbit.apache.org/filevault/vaultfs.html#Binary_Properties specially if (platformPath.endsWith(EXTENSION_BINARY)) { platformPath = Text.getRelativeParent(platformPath, 1); } String repositoryPath = PlatformNameFormat.getRepositoryPath(platformPath, true); if (!repositoryPath.isEmpty()) { // make repository path absolute by prefixing it with "/" in case it is not the root node path itself repositoryPath = "/" + repositoryPath; } else { repositoryPath = "/"; } return repositoryPath; } static <@NotNull T> @NotNull Map<@NotNull String, @NotNull T> filterValidatorsByClass(@NotNull Map<@NotNull String, @NotNull Validator> allValidators, @NotNull Class type) { return allValidators.entrySet().stream() .filter(x -> type.isInstance(x.getValue())) // keep map order .collect(Collectors.toMap( Map.Entry::getKey, x -> type.cast(x.getValue()), (u, v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); }, LinkedHashMap::new )); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy