blob: d887aea0d605cbdd5e2e0c21ab3448bb44f3ce5d [file] [log] [blame]
/*
* 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.spi.impl;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.vault.fs.api.PathFilterSet;
import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
import org.apache.jackrabbit.vault.packaging.PackageProperties;
import org.apache.jackrabbit.vault.packaging.PackageType;
import org.apache.jackrabbit.vault.util.Constants;
import org.apache.jackrabbit.vault.util.DocViewNode2;
import org.apache.jackrabbit.vault.validation.spi.DocumentViewXmlValidator;
import org.apache.jackrabbit.vault.validation.spi.FilterValidator;
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.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.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** Checks if the package type is correctly set for this package
*
* @see <a href="https://issues.apache.org/jira/browse/JCRVLT-170">JCRVLT-170</a> */
public final class PackageTypeValidator implements NodePathValidator, DocumentViewXmlValidator, FilterValidator, PropertiesValidator, MetaInfPathValidator {
private static final String NODETYPE_SLING_OSGI_CONFIG = "sling:OsgiConfig";
protected static final String MESSAGE_FILTER_HAS_INCLUDE_EXCLUDES = "Package of type '%s' is not supposed to contain includes/excludes below any of its filters!";
protected static final String MESSAGE_UNSUPPORTED_SUB_PACKAGE_OF_TYPE = "Package of type '%s' must only contain sub packages of type '%s' but found subpackage of type '%s'!";
protected static final String MESSAGE_UNSUPPORTED_SUB_PACKAGE = "Package of type '%s' is not supposed to contain any subpackages!";
protected static final String MESSAGE_DEPENDENCY = "Package of type '%s' must not have package dependencies but found dependencies '%s'!";
protected static final String MESSAGE_LEGACY_TYPE = "Package of type '%s' is legacy. Use one of the other types instead!";
protected static final String MESSAGE_PACKAGE_HOOKS = "Package of type '%s' must not contain package hooks but has '%s'!";
protected static final String MESSAGE_NO_PACKAGE_TYPE_SET = "No package type set, make sure that property 'packageType' is set in the properties.xml!";
protected static final String MESSAGE_NO_OSGI_BUNDLE_OR_CONFIG_ALLOWED = "Package of type '%s' is not supposed to contain OSGi bundles or configurations!";
protected static final String MESSAGE_ONLY_OSGI_BUNDLE_OR_CONFIG_OR_SUBPACKAGE_ALLOWED = "Package of type '%s' is not supposed to contain anything but OSGi bundles/configurations and subpackages!";
protected static final String MESSAGE_APP_CONTENT = "Package of type '%s' is not supposed to contain content below root nodes %s!";
protected static final String MESSAGE_NO_APP_CONTENT_FOUND = "Package of type '%s' is not supposed to contain content outside root nodes %s!";
protected static final String MESSAGE_PROHIBITED_MUTABLE_PACKAGE_TYPE = "All mutable package types are prohibited and this package is of mutable type '%s'";
protected static final String MESSAGE_PROHIBITED_IMMUTABLE_PACKAGE_TYPE = "All immutable package types are prohibited and this package is of immutable type '%s'";
protected static final String SLING_OSGI_CONFIG = NODETYPE_SLING_OSGI_CONFIG;
protected static final Path PATH_HOOKS = Paths.get(Constants.VAULT_DIR, Constants.HOOKS_DIR);
private final @NotNull PackageType type;
private final @NotNull ValidationMessageSeverity severity;
private final @NotNull ValidationMessageSeverity severityForLegacyType;
private final @NotNull Pattern jcrInstallerNodePathRegex;
private final @NotNull Pattern jcrInstallerAdditionalFileNodePathRegex;
private final @Nullable ValidationContext containerValidationContext;
private final ValidationMessageSeverity severityForNoPackageType;
private final boolean prohibitMutableContent;
private final boolean prohibitImmutableContent;
private final boolean allowComplexFilterRulesInApplicationPackages;
private final boolean allowInstallHooksInApplicationPackages;
private final @NotNull WorkspaceFilter filter;
private final Set<String> immutableRootNodeNames;
private List<String> validContainerNodePaths;
private List<NodeContext> potentiallyDisallowedContainerNodes;
public PackageTypeValidator(@NotNull WorkspaceFilter workspaceFilter, @NotNull ValidationMessageSeverity severity,
@NotNull ValidationMessageSeverity severityForNoPackageType, @NotNull ValidationMessageSeverity severityForLegacyType,
boolean prohibitMutableContent, boolean prohibitImmutableContent, boolean allowComplexFilterRulesInApplicationPackages,
boolean allowInstallHooksInApplicationPackages, @NotNull PackageType type, @NotNull Pattern jcrInstallerNodePathRegex,
Pattern jcrInstallerAdditionalFileNodePathRegex, @NotNull Set<String> immutableRootNodeNames, @Nullable ValidationContext containerValidationContext) {
this.type = type;
this.severity = severity;
this.severityForNoPackageType = severityForNoPackageType;
this.severityForLegacyType = severityForLegacyType;
this.prohibitMutableContent = prohibitMutableContent;
this.prohibitImmutableContent = prohibitImmutableContent;
this.allowComplexFilterRulesInApplicationPackages = allowComplexFilterRulesInApplicationPackages;
this.allowInstallHooksInApplicationPackages = allowInstallHooksInApplicationPackages;
this.jcrInstallerNodePathRegex = jcrInstallerNodePathRegex;
this.jcrInstallerAdditionalFileNodePathRegex = jcrInstallerAdditionalFileNodePathRegex;
this.immutableRootNodeNames = immutableRootNodeNames;
this.containerValidationContext = containerValidationContext;
this.filter = workspaceFilter;
this.validContainerNodePaths = new LinkedList<>();
this.potentiallyDisallowedContainerNodes = new LinkedList<>();
}
private boolean isOsgiBundleOrConfigurationNode(String nodePath, boolean isFileNode) {
if (!jcrInstallerNodePathRegex.matcher(nodePath).matches()) {
return false;
}
if (isFileNode) {
return jcrInstallerAdditionalFileNodePathRegex.matcher(nodePath).matches();
}
return true;
}
static boolean isSubPackage(String nodePath) {
return (nodePath.endsWith(".zip"));
}
boolean isImmutableContent(String nodePath) {
return immutableRootNodeNames.stream().anyMatch(
rootNodeName -> ("/"+rootNodeName).equals(nodePath) || nodePath.startsWith( "/"+rootNodeName + "/"));
}
@Override
public @Nullable Collection<ValidationMessage> done() {
// check if questionable nodes are parents of valid nodes
List<NodeContext> invalidNodes = potentiallyDisallowedContainerNodes.stream().filter(
s -> validContainerNodePaths.stream().noneMatch(
p -> p.startsWith(s.getNodePath() + "/") || p.equals(s.getNodePath())))
.collect(Collectors.toList());
if (!invalidNodes.isEmpty()) {
return invalidNodes.stream().map(
e -> new ValidationMessage(severity, String.format(MESSAGE_ONLY_OSGI_BUNDLE_OR_CONFIG_OR_SUBPACKAGE_ALLOWED, type), e.getNodePath(), e.getFilePath(), e.getBasePath(), null))
.collect(Collectors.toList());
}
return null;
}
@Override
public @Nullable Collection<ValidationMessage> validate(@NotNull NodeContext nodeContext) {
// ignore uncovered nodePaths
if (!filter.covers(nodeContext.getNodePath())) {
return null;
}
Collection<ValidationMessage> messages = new LinkedList<>();
switch (type) {
case CONTENT:
if (isImmutableContent(nodeContext.getNodePath())) {
messages.add(new ValidationMessage(severity, String.format(MESSAGE_APP_CONTENT, type, immutableRootNodeNames.stream().collect(Collectors.joining("' or '", "'", "'")))));
}
if (isOsgiBundleOrConfigurationNode(nodeContext.getNodePath(), true)) {
messages.add(new ValidationMessage(severity, String.format(MESSAGE_NO_OSGI_BUNDLE_OR_CONFIG_ALLOWED, type)));
}
break;
case APPLICATION:
if (!isImmutableContent(nodeContext.getNodePath())) {
messages.add(new ValidationMessage(severity, String.format(MESSAGE_NO_APP_CONTENT_FOUND, type, immutableRootNodeNames.stream().collect(Collectors.joining("' or '", "'", "'")))));
}
if (isOsgiBundleOrConfigurationNode(nodeContext.getNodePath(), true)) {
messages.add(new ValidationMessage(severity, String.format(MESSAGE_NO_OSGI_BUNDLE_OR_CONFIG_ALLOWED, type)));
}
// sub packages are detected via validate(Properties) on the sub package
break;
case CONTAINER:
// sling:OsgiConfig
if (isOsgiBundleOrConfigurationNode(nodeContext.getNodePath(), true)) {
validContainerNodePaths.add(nodeContext.getNodePath());
}
else if (isSubPackage(nodeContext.getNodePath())) {
validContainerNodePaths.add(nodeContext.getNodePath());
} else {
// only potentially disallowed, as the node may be a parent of a sub package or osgi bundle, which is allowed as well
potentiallyDisallowedContainerNodes.add(nodeContext);
}
break;
case MIXED:
// no validations currently as most relaxed type
break;
}
return messages;
}
@Override
public Collection<ValidationMessage> validate(@NotNull WorkspaceFilter filter) {
switch (type) {
case APPLICATION:
if (!allowComplexFilterRulesInApplicationPackages && hasIncludesOrExcludes(filter)) {
return Collections.singleton(new ValidationMessage(severity, String.format(MESSAGE_FILTER_HAS_INCLUDE_EXCLUDES, type)));
}
break;
case CONTENT:
case CONTAINER:
case MIXED:
break;
}
return null;
}
@Override
public Collection<ValidationMessage> validate(@NotNull PackageProperties properties) {
PackageType packageType = properties.getPackageType();
if (packageType == null) {
return Collections.singleton(new ValidationMessage(severityForNoPackageType, MESSAGE_NO_PACKAGE_TYPE_SET));
}
Collection<ValidationMessage> messages = new LinkedList<>();
// is sub package?
if (containerValidationContext != null) {
messages.add(new ValidationMessage(ValidationMessageSeverity.DEBUG, "Found sub package"));
ValidationMessage message = validateSubPackageType(properties.getPackageType(),
containerValidationContext.getProperties().getPackageType());
if (message != null) {
messages.add(message);
}
}
switch (packageType) {
case APPLICATION:
// must not contain hooks (this detects external hooks)
if (!properties.getExternalHooks().isEmpty() && !allowInstallHooksInApplicationPackages) {
messages.add(new ValidationMessage(severity,
String.format(MESSAGE_PACKAGE_HOOKS, properties.getPackageType(), properties.getExternalHooks())));
}
if (prohibitImmutableContent) {
messages.add(new ValidationMessage(severity,
String.format(MESSAGE_PROHIBITED_IMMUTABLE_PACKAGE_TYPE, properties.getPackageType())));
}
break;
case CONTENT:
if (prohibitMutableContent) {
messages.add(new ValidationMessage(severity,
String.format(MESSAGE_PROHIBITED_MUTABLE_PACKAGE_TYPE, properties.getPackageType())));
}
break;
case CONTAINER:
// no dependencies
if (properties.getDependencies() != null && properties.getDependencies().length > 0) {
messages.add(new ValidationMessage(severity,
String.format(MESSAGE_DEPENDENCY, properties.getPackageType(), StringUtils.join(properties.getDependencies()))));
}
if (prohibitImmutableContent) {
messages.add(new ValidationMessage(ValidationMessageSeverity.ERROR,
String.format(MESSAGE_PROHIBITED_IMMUTABLE_PACKAGE_TYPE, properties.getPackageType())));
}
break;
case MIXED:
messages.add(
new ValidationMessage(severityForLegacyType, String.format(MESSAGE_LEGACY_TYPE, properties.getPackageType())));
if (prohibitImmutableContent) {
messages.add(new ValidationMessage(ValidationMessageSeverity.ERROR,
String.format(MESSAGE_PROHIBITED_IMMUTABLE_PACKAGE_TYPE, properties.getPackageType())));
}
if (prohibitMutableContent) {
messages.add(new ValidationMessage(ValidationMessageSeverity.ERROR,
String.format(MESSAGE_PROHIBITED_MUTABLE_PACKAGE_TYPE, properties.getPackageType())));
}
break;
}
return messages;
}
@Override
public @Nullable Collection<ValidationMessage> validate(@NotNull DocViewNode2 node, @NotNull NodeContext nodeContext,
boolean isRoot) {
Collection<ValidationMessage> messages = new LinkedList<>();
switch (type) {
case CONTENT:
case APPLICATION:
// is it sling:OsgiConfig node?
if (node.getPrimaryType().isPresent() && NODETYPE_SLING_OSGI_CONFIG.equals(node.getPrimaryType().orElse("")) && isOsgiBundleOrConfigurationNode(nodeContext.getNodePath(), false)) {
messages.add(new ValidationMessage(severity, String.format(MESSAGE_NO_OSGI_BUNDLE_OR_CONFIG_ALLOWED, type)));
}
break;
case CONTAINER:
if (node.getPrimaryType().isPresent() && NODETYPE_SLING_OSGI_CONFIG.equals(node.getPrimaryType().orElse("")) && isOsgiBundleOrConfigurationNode(nodeContext.getNodePath(), false)) {
validContainerNodePaths.add(nodeContext.getNodePath());
}
break;
case MIXED:
// no validations currently as most relaxed type
break;
}
return messages;
}
static boolean hasIncludesOrExcludes(WorkspaceFilter filter) {
for (PathFilterSet set : filter.getFilterSets()) {
if (!set.getEntries().isEmpty()) {
return true;
}
}
return false;
}
private ValidationMessage validateSubPackageType(PackageType packageType, @Nullable PackageType containerPackageType) {
ValidationMessage message = null;
if (containerPackageType == null) {
return null;
}
switch (containerPackageType) {
case APPLICATION:
// no sub packages allowed
message = new ValidationMessage(severity, String.format(MESSAGE_UNSUPPORTED_SUB_PACKAGE, containerPackageType));
break;
case CONTENT:
if (packageType != PackageType.CONTENT) {
message = new ValidationMessage(severity, String.format(MESSAGE_UNSUPPORTED_SUB_PACKAGE_OF_TYPE, containerPackageType,
PackageType.CONTENT.toString(), packageType));
}
break;
case CONTAINER:
if (packageType == PackageType.MIXED) {
message = new ValidationMessage(severityForLegacyType, String.format(MESSAGE_UNSUPPORTED_SUB_PACKAGE_OF_TYPE, containerPackageType,
StringUtils.join(new String[] { PackageType.APPLICATION.toString(), PackageType.CONTENT.toString(),
PackageType.CONTAINER.toString() }, ", "),
packageType));
}
break;
case MIXED:
break;
}
return message;
}
@Override
public Collection<ValidationMessage> validateMetaInfPath(@NotNull Path filePath, @NotNull Path basePath, boolean isFolder) {
switch (type) {
case APPLICATION:
if (filePath.startsWith(PATH_HOOKS) && !allowInstallHooksInApplicationPackages)
// must not contain hooks (this detects internal hooks)
return Collections.singleton(new ValidationMessage(severity, String.format(MESSAGE_PACKAGE_HOOKS, type, filePath)));
default:
break;
}
return null;
}
}