blob: 8a5f81df3ad6bd117988b7929164a54716513458 [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.sling.scriptingbundle.plugin.processor;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.vault.util.PlatformNameFormat;
import org.apache.sling.api.resource.type.ResourceType;
import org.apache.sling.scriptingbundle.plugin.capability.ProvidedResourceTypeCapability;
import org.apache.sling.scriptingbundle.plugin.capability.RequiredResourceTypeCapability;
import org.apache.sling.scriptingbundle.plugin.processor.filevault.VaultContentXmlReader;
import org.jetbrains.annotations.NotNull;
import org.osgi.framework.VersionRange;
import aQute.bnd.header.Attrs;
import aQute.bnd.header.OSGiHeader;
import aQute.bnd.header.Parameters;
public class FileProcessor {
private final Logger log;
private final Set<String> searchPaths;
private final Map<String, String> scriptEngineMappings;
private static final Collection<String> EXTENDS_ALLOWED_ATTRIBUTE_NAMES = Arrays.asList(aQute.bnd.osgi.Constants.RESOLUTION_DIRECTIVE, aQute.bnd.osgi.Constants.VERSION_ATTRIBUTE);
private static final Collection<String> REQUIRES_ALLOWED_ATTRIBUTE_NAMES = Arrays.asList(aQute.bnd.osgi.Constants.RESOLUTION_DIRECTIVE, aQute.bnd.osgi.Constants.VERSION_ATTRIBUTE);
public FileProcessor(Logger log, Set<String> searchPaths, Map<String, String> scriptEngineMappings) {
this.log = log;
this.searchPaths = searchPaths;
this.scriptEngineMappings = scriptEngineMappings;
}
public void processExtendsFile(@NotNull ResourceType resourceType, @NotNull Path file,
@NotNull Set<ProvidedResourceTypeCapability> providedCapabilities,
@NotNull Set<RequiredResourceTypeCapability> requiredCapabilities) throws IllegalArgumentException {
try {
List<String> extendedResources = Files.readAllLines(file, StandardCharsets.UTF_8);
if (extendedResources.size() == 1) {
processExtendedResourceType(resourceType, file, providedCapabilities, requiredCapabilities, extendedResources.get(0));
} else {
throw new IllegalArgumentException(String.format("The file '%s' must contain one line only (not multiple ones)", file));
}
} catch (IOException e) {
throw new UncheckedIOException(String.format("Unable to read file %s.", file.toString()), e);
}
}
private void processExtendedResourceType(@NotNull ResourceType resourceType, @NotNull Path extendsFile,
@NotNull Set<ProvidedResourceTypeCapability> providedCapabilities,
@NotNull Set<RequiredResourceTypeCapability> requiredCapabilities, @NotNull String extendedResource) {
Parameters parameters = OSGiHeader.parseHeader(extendedResource);
if (parameters.size() != 1) {
throw new IllegalArgumentException(String.format("The file '%s' must contain one clause only (not multiple ones separated by ',')",
extendsFile));
}
Entry<String, Attrs> extendsParameter = parameters.entrySet().iterator().next();
for (String attributeName : extendsParameter.getValue().keySet()) {
if (!EXTENDS_ALLOWED_ATTRIBUTE_NAMES.contains(attributeName)) {
throw new IllegalArgumentException(String.format("Found unsupported attribute/directive '%s' in file '%s'. Only the following attributes or directives may be used in the extends file: %s", attributeName,
extendsFile, String.join(",", EXTENDS_ALLOWED_ATTRIBUTE_NAMES)));
}
}
String extendedResourceType = FilenameUtils.normalize(extendsParameter.getKey(), true);
boolean isOptional = aQute.bnd.osgi.Constants.OPTIONAL.equals(extendsParameter.getValue().get(aQute.bnd.osgi.Constants.RESOLUTION_DIRECTIVE));
Set<String> searchPathResourceTypes = processSearchPathResourceTypes(resourceType);
Optional<ProvidedResourceTypeCapability> rootCapability = providedCapabilities.stream().filter(capability ->
capability.getResourceTypes().equals(searchPathResourceTypes) && capability.getSelectors().isEmpty() &&
StringUtils.isEmpty(capability.getRequestMethod()) && StringUtils.isEmpty(capability.getRequestExtension())
).findFirst();
rootCapability.ifPresent(capability -> {
providedCapabilities.remove(capability);
ProvidedResourceTypeCapability replacement =
ProvidedResourceTypeCapability.builder().fromCapability(capability)
.withExtendsResourceType(extendedResourceType).build();
providedCapabilities.add(replacement);
});
if (!rootCapability.isPresent()) {
providedCapabilities.add(
ProvidedResourceTypeCapability.builder()
.withResourceTypes(processSearchPathResourceTypes(resourceType))
.withVersion(resourceType.getVersion())
.withExtendsResourceType(extendedResourceType)
.build());
}
RequiredResourceTypeCapability.Builder requiredBuilder =
RequiredResourceTypeCapability.builder().withResourceType(extendedResourceType);
if (isOptional) {
requiredBuilder.withIsOptional();
}
extractVersionRange(extendsFile, requiredBuilder, extendsParameter.getValue().getVersion());
requiredCapabilities.add(requiredBuilder.build());
}
void processRequiresFile(@NotNull Path requiresFile,
@NotNull Set<RequiredResourceTypeCapability> requiredCapabilities) {
try {
List<String> requiredResourceTypes = Files.readAllLines(requiresFile, StandardCharsets.UTF_8);
processRequiredResourceTypes(requiresFile, requiredCapabilities, requiredResourceTypes);
} catch (IOException e) {
throw new UncheckedIOException(String.format("Unable to read file %s.", requiresFile), e);
}
}
private void processRequiredResourceTypes(@NotNull Path requiresFile, @NotNull Set<RequiredResourceTypeCapability> requiredCapabilities,
List<String> requiredResourceTypes) {
for (String requiredResourceType : requiredResourceTypes) {
Parameters parameters = OSGiHeader.parseHeader(requiredResourceType);
if (parameters.size() != 1) {
throw new IllegalArgumentException(String.format("Each line in file '%s' must contain one clause only (not multiple ones separated by ',')",
requiresFile));
}
Entry<String, Attrs> requiresParameter = parameters.entrySet().iterator().next();
for (String attributeName : requiresParameter.getValue().keySet()) {
if (!REQUIRES_ALLOWED_ATTRIBUTE_NAMES.contains(attributeName)) {
throw new IllegalArgumentException(String.format("Found unsupported attribute/directive '%s' in file '%s'. Only the following attributes or directives may be used in the requires file: %s", attributeName,
requiresFile, String.join(",", REQUIRES_ALLOWED_ATTRIBUTE_NAMES)));
}
}
String resourceType = FilenameUtils.normalize(requiresParameter.getKey(), true);
boolean isOptional = aQute.bnd.osgi.Constants.OPTIONAL.equals(requiresParameter.getValue().get(aQute.bnd.osgi.Constants.RESOLUTION_DIRECTIVE));
RequiredResourceTypeCapability.Builder requiredBuilder =
RequiredResourceTypeCapability.builder().withResourceType(resourceType);
if (isOptional) {
requiredBuilder.withIsOptional();
}
extractVersionRange(requiresFile, requiredBuilder, requiresParameter.getValue().getVersion());
requiredCapabilities.add(requiredBuilder.build());
}
}
public void processScriptFile(@NotNull Path resourceTypeDirectory, @NotNull Path scriptPath,
@NotNull ResourceType resourceType, @NotNull Set<ProvidedResourceTypeCapability> providedCapabilities,
boolean inContentPackage) {
String filePath = scriptPath.toString();
String extension = FilenameUtils.getExtension(filePath);
if (StringUtils.isNotEmpty(extension)) {
Path scriptFile = scriptPath.getFileName();
if (scriptFile != null) {
Path relativeResourceTypeFolder = resourceTypeDirectory.relativize(scriptPath);
int pathSegments = relativeResourceTypeFolder.getNameCount();
List<String> selectors = new ArrayList<>();
if (pathSegments > 1) {
for (int i = 0; i < pathSegments - 1; i++) {
selectors.add(inContentPackage ?
PlatformNameFormat.getRepositoryName(relativeResourceTypeFolder.getName(i).toString()) :
relativeResourceTypeFolder.getName(i).toString()
);
}
}
String scriptFileName = scriptFile.toString();
Script script = Script.parseScript(inContentPackage ? PlatformNameFormat.getRepositoryPath(scriptFileName) : scriptFileName);
if (script != null) {
String scriptEngine = scriptEngineMappings.get(script.getScriptExtension());
if (scriptEngine != null) {
String scriptName = script.getName();
Set<String> searchPathProcessesResourceTypes = processSearchPathResourceTypes(resourceType);
if (scriptName != null && !resourceType.getResourceLabel().equals(scriptName)) {
selectors.add(script.getName());
}
Optional<ProvidedResourceTypeCapability> extendsCapability = Optional.empty();
if (selectors.isEmpty() && StringUtils.isEmpty(script.getRequestExtension()) &&
StringUtils.isEmpty(script.getRequestMethod())) {
extendsCapability =
providedCapabilities.stream()
.filter(capability -> StringUtils.isNotEmpty(capability.getExtendsResourceType()) &&
capability.getResourceTypes().equals(searchPathProcessesResourceTypes) &&
capability.getSelectors().isEmpty() &&
StringUtils.isEmpty(capability.getRequestExtension()) &&
StringUtils.isEmpty(capability.getRequestMethod())).findAny();
}
ProvidedResourceTypeCapability.Builder builder = ProvidedResourceTypeCapability.builder()
.withResourceTypes(searchPathProcessesResourceTypes)
.withVersion(resourceType.getVersion())
.withSelectors(selectors)
.withRequestExtension(script.getRequestExtension())
.withRequestMethod(script.getRequestMethod())
.withScriptEngine(scriptEngine)
.withScriptExtension(script.getScriptExtension());
extendsCapability.ifPresent(capability -> {
builder.withExtendsResourceType(capability.getExtendsResourceType());
providedCapabilities.remove(capability);
}
);
providedCapabilities.add(builder.build());
} else {
log.warn(String.format("Cannot find a script engine mapping for script %s.", scriptPath));
}
} else {
log.warn(String.format("Skipping file %s not denoting a script as it does not follow the filename patterns outlined at https://sling.apache.org/documentation/the-sling-engine/url-to-script-resolution.html#script-naming-conventions", scriptPath));
}
} else {
throw new IllegalArgumentException(String.format("Invalid path given: '%s'.", scriptPath));
}
}
}
private Set<String> processSearchPathResourceTypes(@NotNull ResourceType resourceType) {
Set<String> resourceTypes = new HashSet<>();
for (String searchPath : searchPaths) {
if (!searchPath.endsWith("/")) {
searchPath = searchPath + "/";
}
String absoluteType = "/" + resourceType.getType();
if (absoluteType.startsWith(searchPath)) {
resourceTypes.add(absoluteType);
resourceTypes.add(absoluteType.substring(searchPath.length()));
}
}
if (resourceTypes.isEmpty()) {
resourceTypes.add(resourceType.getType());
}
return resourceTypes;
}
private void extractVersionRange(@NotNull Path requiresFile, @NotNull RequiredResourceTypeCapability.Builder requiredBuilder,
String version) {
try {
if (version != null) {
requiredBuilder.withVersionRange(VersionRange.valueOf(version.substring(version.indexOf('=') + 1)));
}
} catch (IllegalArgumentException ignored) {
log.warn(String.format("Invalid version range format %s in file %s.", version, requiresFile));
}
}
public void processVaultFile(@NotNull Path entry, @NotNull ResourceType resourceType,
@NotNull Set<ProvidedResourceTypeCapability> providedCapabilities,
@NotNull Set<RequiredResourceTypeCapability> requiredCapabilities) {
try {
VaultContentXmlReader reader = new VaultContentXmlReader(entry);
Optional<String> slingResourceSuperType = reader.getSlingResourceSuperType();
slingResourceSuperType.ifPresent(
resourceSuperType -> processExtendedResourceType(resourceType, entry, providedCapabilities, requiredCapabilities,
resourceSuperType));
if (!reader.getSlingRequiredResourceTypes().isEmpty()) {
processRequiredResourceTypes(entry, requiredCapabilities, new ArrayList<>(reader.getSlingRequiredResourceTypes()));
}
} catch (IOException e) {
throw new UncheckedIOException(String.format("Unable to read file %s.", entry), e);
}
}
}