blob: 2c931538b44ee36475507a57c05487b685061520 [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.feature.cpconverter.features;
import static java.util.Objects.requireNonNull;
import static org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter.ZIP_TYPE;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Stack;
import org.apache.jackrabbit.vault.packaging.PackageId;
import org.apache.jackrabbit.vault.packaging.VaultPackage;
import org.apache.sling.feature.Artifact;
import org.apache.sling.feature.ArtifactId;
import org.apache.sling.feature.Artifacts;
import org.apache.sling.feature.Configuration;
import org.apache.sling.feature.Extension;
import org.apache.sling.feature.ExtensionState;
import org.apache.sling.feature.ExtensionType;
import org.apache.sling.feature.Extensions;
import org.apache.sling.feature.Feature;
import org.apache.sling.feature.cpconverter.ConverterException;
import org.apache.sling.feature.cpconverter.accesscontrol.AclManager;
import org.apache.sling.feature.cpconverter.accesscontrol.Mapping;
import org.apache.sling.feature.cpconverter.index.IndexManager;
import org.apache.sling.feature.cpconverter.interpolator.SimpleVariablesInterpolator;
import org.apache.sling.feature.cpconverter.interpolator.VariablesInterpolator;
import org.apache.sling.feature.cpconverter.repoinit.NoOpVisitor;
import org.apache.sling.feature.cpconverter.vltpkg.PackagesEventsEmitter;
import org.apache.sling.feature.extension.apiregions.api.ApiExport;
import org.apache.sling.feature.extension.apiregions.api.ApiRegion;
import org.apache.sling.feature.extension.apiregions.api.ApiRegions;
import org.apache.sling.feature.io.json.FeatureJSONWriter;
import org.apache.sling.repoinit.parser.RepoInitParsingException;
import org.apache.sling.repoinit.parser.impl.RepoInitParserService;
import org.apache.sling.repoinit.parser.operations.Operation;
import org.apache.sling.repoinit.parser.operations.RegisterNamespace;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.osgi.framework.Constants;
import org.osgi.util.converter.Converters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DefaultFeaturesManager implements FeaturesManager, PackagesEventsEmitter {
public enum ConfigurationHandling {
ORDERED,
MERGE,
STRICT
}
private static final String CONTENT_PACKAGES = "content-packages";
private static final String SLING_OSGI_FEATURE_TILE_TYPE = "slingosgifeature";
private static final String JSON_FILE_EXTENSION = ".json";
private static final String BUNDLE_ORIGINS = "content-package-origins";
private static final String CONFIGURATION_ORIGINS = Configuration.CONFIGURATOR_PREFIX.concat(BUNDLE_ORIGINS);
private final Logger logger = LoggerFactory.getLogger(getClass());
private final Map<String, Feature> runModes = new HashMap<>();
private final VariablesInterpolator interpolator = new SimpleVariablesInterpolator();
private final ConfigurationHandling configurationHandling;
private final int bundlesStartOrder;
private final File featureModelsOutputDirectory;
private final Map<String, List<String>> apiRegionExports = new HashMap<>();
private final String artifactIdOverride;
private final String prefix;
private final Map<String, String> properties;
private final List<String> targetAPIRegions = new ArrayList<>();
private final Map<String, String> namespaceUriByPrefix;
private String exportsToAPIRegion;
private Feature targetFeature;
private AclManager aclManager;
private final Map<String, String> pidToPathMapping = new HashMap<>();
private final Stack<String> packageIds = new Stack<>();
DefaultFeaturesManager() {
this(new File(""));
}
public DefaultFeaturesManager(@NotNull File tempDir) {
this(true, 20, tempDir, null, null, new HashMap<>(), null);
}
public DefaultFeaturesManager(boolean mergeConfigurations,
int bundlesStartOrder,
@NotNull File featureModelsOutputDirectory,
@Nullable String artifactIdOverride,
@Nullable String prefix,
@NotNull Map<String, String> properties,
@Nullable AclManager aclManager) {
this(mergeConfigurations ? ConfigurationHandling.MERGE : ConfigurationHandling.ORDERED, bundlesStartOrder, featureModelsOutputDirectory,
artifactIdOverride, prefix, properties, aclManager);
}
public DefaultFeaturesManager(@NotNull ConfigurationHandling configurationHandling,
int bundlesStartOrder,
@NotNull File featureModelsOutputDirectory,
@Nullable String artifactIdOverride,
@Nullable String prefix,
@NotNull Map<String, String> properties,
@Nullable AclManager aclManager) {
this.configurationHandling = configurationHandling;
this.bundlesStartOrder = bundlesStartOrder;
this.featureModelsOutputDirectory = featureModelsOutputDirectory;
this.artifactIdOverride = artifactIdOverride;
this.prefix = prefix;
this.properties = properties;
this.aclManager = aclManager;
this.namespaceUriByPrefix = new HashMap<>();
}
@Override
public void init(@NotNull ArtifactId packageId) {
targetFeature = new Feature(packageId.changeClassifier(null).changeType(SLING_OSGI_FEATURE_TILE_TYPE));
runModes.clear();
apiRegionExports.clear();
}
@Override
public @Nullable Feature getTargetFeature() {
return targetFeature;
}
@Override
public @NotNull Feature getRunMode(@Nullable String runMode) {
if (getTargetFeature() == null) {
throw new IllegalStateException("Target feature not initialized yet, please make sure convert() method was invoked first.");
}
if (runMode == null) {
return getTargetFeature();
}
ArtifactId newId = appendRunmode(getTargetFeature().getId(), runMode);
return runModes.computeIfAbsent(runMode, k -> new Feature(newId));
}
@Override
public void addArtifact(@Nullable String runMode, @NotNull ArtifactId id) {
requireNonNull(id, "Artifact can not be attached to a feature without specifying a valid ArtifactId.");
addArtifact(runMode, new Artifact(id), null);
}
@Override
public void addArtifact(@Nullable String runMode, @NotNull Artifact artifact, @Nullable Integer startOrder) {
requireNonNull(artifact, "Null artifact can not be attached to a feature.");
Feature feature = getRunMode(runMode);
Artifacts artifacts;
if (ZIP_TYPE.equals(artifact.getId().getType())) {
Extensions extensions = feature.getExtensions();
Extension extension = extensions.getByName(CONTENT_PACKAGES);
if (extension == null) {
extension = new Extension(ExtensionType.ARTIFACTS, CONTENT_PACKAGES, ExtensionState.REQUIRED);
extensions.add(extension);
}
artifacts = extension.getArtifacts();
} else {
// set start order
final int startOrderForBundle = startOrder != null ? startOrder : bundlesStartOrder;
artifact.setStartOrder(startOrderForBundle);
// set origins
if (!this.packageIds.isEmpty()) {
artifact.getMetadata().put(BUNDLE_ORIGINS, String.join("|", this.packageIds));
}
artifacts = feature.getBundles();
}
artifacts.add(artifact);
}
private static @NotNull ArtifactId appendRunmode(@NotNull ArtifactId id, @Nullable String runMode) {
ArtifactId newId;
if (runMode == null) {
newId = id;
} else {
final String classifier;
if (id.getClassifier() != null && !id.getClassifier().isEmpty()) {
classifier = id.getClassifier() + '-' + runMode;
} else {
classifier = runMode;
}
newId = new ArtifactId(id.getGroupId(), id.getArtifactId(), id.getVersion(), classifier, id.getType());
}
return newId;
}
@Override
public void addAPIRegionExport(@Nullable String runMode, @NotNull String exportedPackage) {
if (exportsToAPIRegion == null) {
return; // Ignore if we're not exporting to an API region
}
getRunMode(runMode); // Trigger runmode initialization
List<String> l = apiRegionExports.computeIfAbsent(runMode, r -> new ArrayList<>());
l.add(exportedPackage);
}
private static final String REPOINIT_FACTORY_PID = "org.apache.sling.jcr.repoinit.RepositoryInitializer";
private static final String REPOINIT_PID = "org.apache.sling.jcr.repoinit.impl.RepositoryInitializer";
private static final String SERVICE_USER_MAPPING_PID = "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl";
private boolean enforceServiceMappingByPrincipal;
public void setEnforceServiceMappingByPrincipal(boolean enforceServiceMappingByPrincipal) {
this.enforceServiceMappingByPrincipal = enforceServiceMappingByPrincipal;
}
public void addSeed(@NotNull Feature seed) throws IOException, ConverterException {
for (Configuration conf : seed.getConfigurations()) {
handleRepoinitAndMappings("seed", conf, conf.getConfigurationProperties(), false);
}
if (seed.getExtensions().getByName(Extension.EXTENSION_NAME_REPOINIT) != null) {
String repoInitText = seed.getExtensions().getByName(Extension.EXTENSION_NAME_REPOINIT).getText();
getAclManager().addRepoinitExtention("seed", repoInitText, "seed", this);
extractNamespaces(repoInitText, namespaceUriByPrefix);
}
}
private static void extractNamespaces(String repoInitText, Map<String, String> namespaceUriByPrefix) {
try {
List<Operation> ops = new RepoInitParserService().parse(new StringReader(repoInitText));
for (Operation op : ops) {
op.accept(new NoOpVisitor() {
@Override
public void visitRegisterNamespace(RegisterNamespace registerNamespace) {
namespaceUriByPrefix.put(registerNamespace.getPrefix(), registerNamespace.getURI());
}
});
}
} catch (RepoInitParsingException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public @NotNull Map<String, String> getNamespaceUriByPrefix() {
return namespaceUriByPrefix;
}
@NotNull AclManager getAclManager() {
return Objects.requireNonNull(this.aclManager);
}
public void setAclManager(AclManager aclManager) {
this.aclManager = aclManager;
}
private boolean handleRepoinitAndMappings(String runMode, Configuration cfg, Dictionary<String, Object> configurationProperties, boolean enforceServiceMappingByPrincipal)
throws IOException, ConverterException {
if (REPOINIT_FACTORY_PID.equals(cfg.getFactoryPid())) {
final String[] scripts = Converters.standardConverter().convert(configurationProperties.get("scripts")).to(String[].class);
if (scripts != null && scripts.length > 0) {
getAclManager().addRepoinitExtention(cfg.getPid(), String.join(System.lineSeparator(), scripts), runMode, this);
}
checkReferences(configurationProperties, cfg.getPid());
return true;
} else if (REPOINIT_PID.equals(cfg.getPid())) {
checkReferences(configurationProperties, cfg.getPid());
return true;
} else if (cfg.getPid().startsWith(SERVICE_USER_MAPPING_PID)) {
String[] mappings = Converters.standardConverter().convert(configurationProperties.get("user.mapping")).to(String[].class);
List<String> newMappings = convertMappings(mappings, cfg.getPid(), enforceServiceMappingByPrincipal);
// replace 'user.mapping' property by the new mappings, which may have been refactored
if (!newMappings.isEmpty()) {
configurationProperties.put("user.mapping", newMappings.toArray(new String[0]));
}
}
return false;
}
private List<String> convertMappings(@Nullable String[] mappings, @NotNull String pid, boolean enforceServiceMappingByPrincipal) throws ConverterException {
if (mappings == null) {
return Collections.emptyList();
}
List<String> newMappings = new ArrayList<>();
for (String usermapping : mappings) {
if (usermapping == null || usermapping.trim().isEmpty()) {
logger.warn("ServiceUserMapping: Ignoring empty mapping in {}", pid);
// invalid empty mapping => ignore
continue;
}
try {
Mapping mapping = new Mapping(usermapping, enforceServiceMappingByPrincipal);
getAclManager().addMapping(mapping);
newMappings.add(mapping.asString());
} catch (IllegalArgumentException iae) {
throw new ConverterException("ServiceUserMapping: Detected invalid mapping in " + pid);
}
}
return newMappings;
}
@Override
public void addConfiguration(@Nullable String runMode,
@NotNull Configuration cfg,
@NotNull String path,
@NotNull Dictionary<String, Object> configurationProperties)
throws IOException, ConverterException {
if (handleRepoinitAndMappings(runMode, cfg, configurationProperties, enforceServiceMappingByPrincipal)) {
return;
}
Feature feature = getRunMode(runMode);
Configuration configuration = feature.getConfigurations().getConfiguration(cfg.getPid());
if (configuration == null) {
configuration = new Configuration(cfg.getPid());
feature.getConfigurations().add(configuration);
this.pidToPathMapping.put(cfg.getPid(), path);
} else {
switch (this.configurationHandling) {
case STRICT:
throw new ConverterException("Configuration '"
+ cfg.getPid()
+ "' already defined in Feature Model '"
+ feature.getId().toMvnId()
+ "', set the 'mergeConfigurations' flag to 'true' if you want to merge multiple configurations with same PID");
case ORDERED:
final String oldPath = this.pidToPathMapping.get(cfg.getPid());
if (oldPath == null || oldPath.compareTo(path) > 0) {
this.pidToPathMapping.put(cfg.getPid(), path);
feature.getConfigurations().remove(configuration);
configuration = new Configuration(cfg.getPid());
feature.getConfigurations().add(configuration);
} else {
return;
}
break;
case MERGE: // nothing to do
}
}
adjustConfigurationProperties(configuration, configurationProperties);
}
private void adjustConfigurationProperties(@NotNull Configuration configuration,
@NotNull Dictionary<String, Object> configurationProperties) {
Enumeration<String> keys = configurationProperties.keys();
while (keys.hasMoreElements()) {
String key = keys.nextElement();
Object value = configurationProperties.get(key);
if (value instanceof Collection) {
value = ((Collection<?>) value).toArray();
}
configuration.getProperties().put(key, value);
}
// remove internal properties (these are ignored anyway)
configuration.getProperties().remove(Constants.SERVICE_PID);
configuration.getProperties().remove("service.bundleLocation");
configuration.getProperties().remove("service.factoryPid");
// set origins
if (!this.packageIds.isEmpty()) {
final List<String> origins = new ArrayList<>();
final Object val = configuration.getProperties().get(CONFIGURATION_ORIGINS);
if (val != null) {
origins.addAll(Arrays.asList(val.toString().split(",")));
}
origins.add(String.join("|", this.packageIds));
configuration.getProperties().put(CONFIGURATION_ORIGINS, String.join(",", origins));
}
}
private void addAPIRegions(@NotNull Feature feature, @Nullable List<String> exportedPackages) throws IOException {
if (exportedPackages == null)
exportedPackages = Collections.emptyList();
if (exportedPackages.isEmpty() && targetAPIRegions.isEmpty())
return; // Nothing to do.
ApiRegions regions = new ApiRegions();
if (exportsToAPIRegion != null) {
ApiRegion ar = new ApiRegion(exportsToAPIRegion);
exportedPackages
.stream()
.forEach(e -> ar.add(new ApiExport(e)));
regions.add(ar);
}
targetAPIRegions
.stream()
.forEach(r -> regions.add(new ApiRegion(r)));
Extension apiRegions = new Extension(ExtensionType.JSON, "api-regions", ExtensionState.OPTIONAL);
apiRegions.setJSONStructure(regions.toJSONArray());
feature.getExtensions().add(apiRegions);
}
@Override
public void serialize() throws IOException {
RunmodeMapper runmodeMapper = RunmodeMapper.open(featureModelsOutputDirectory);
serialize(targetFeature, null, runmodeMapper);
if (!runModes.isEmpty()) {
for (Entry<String, Feature> runmodeEntry : runModes.entrySet()) {
String runmode = runmodeEntry.getKey();
serialize(runmodeEntry.getValue(), runmode, runmodeMapper);
}
}
runmodeMapper.save();
}
private void serialize(Feature feature, String runMode, RunmodeMapper runmodeMapper) throws IOException {
addAPIRegions(feature, apiRegionExports.get(runMode));
StringBuilder fileNameBuilder = new StringBuilder()
.append((prefix != null) ? prefix : "")
.append(feature.getId().getArtifactId());
String classifier = feature.getId().getClassifier();
if (classifier != null && !classifier.isEmpty()) {
fileNameBuilder.append('-').append(classifier);
}
if (properties != null) {
properties.put("filename", fileNameBuilder.toString());
}
fileNameBuilder.append(JSON_FILE_EXTENSION);
String fileName = fileNameBuilder.toString();
File targetFile = new File(featureModelsOutputDirectory, fileName);
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();
}
if (artifactIdOverride != null && !artifactIdOverride.isEmpty()) {
String interpolatedIdOverride = interpolator.interpolate(artifactIdOverride, properties);
ArtifactId idOverrride = appendRunmode(ArtifactId.parse(interpolatedIdOverride), runMode);
feature = feature.copy(idOverrride);
}
logger.info("Writing resulting Feature Model '{}' to file '{}'...", feature.getId(), targetFile);
try (FileWriter targetWriter = new FileWriter(targetFile)) {
FeatureJSONWriter.write(targetWriter, feature);
logger.info("'{}' Feature File successfully written!", targetFile);
runmodeMapper.addOrUpdate(runMode, fileName);
}
}
public synchronized @NotNull DefaultFeaturesManager setAPIRegions(@NotNull List<String> regions) {
targetAPIRegions.clear();
targetAPIRegions.addAll(regions);
return this;
}
public synchronized @NotNull DefaultFeaturesManager setExportToAPIRegion(@NotNull String region) {
exportsToAPIRegion = region;
return this;
}
@Override
public void addOrAppendRepoInitExtension(@NotNull String source, @NotNull String text, @Nullable String runMode) {
if (runMode == null) {
logger.info("Adding global repo-init");
} else {
logger.info("Adding repo-init for run mode: {}", runMode);
}
// prepend source/origin
text = "# origin=".concat(String.join("|", this.packageIds)).concat(" source=").concat(source).concat(System.lineSeparator()).concat(text);
Extension repoInitExtension = getRunMode(runMode).getExtensions().getByName(Extension.EXTENSION_NAME_REPOINIT);
if (repoInitExtension == null) {
repoInitExtension = new Extension(ExtensionType.TEXT, Extension.EXTENSION_NAME_REPOINIT, ExtensionState.REQUIRED);
getRunMode(runMode).getExtensions().add(repoInitExtension);
repoInitExtension.setText(text);
} else {
repoInitExtension.setText(repoInitExtension.getText().concat(System.lineSeparator()).concat(text));
}
}
@Override
public void addOrAppendOakIndexDefinitionsExtension(String source, String text)
throws IOException, ConverterException {
Extension oakIndexDefsExtension = getRunMode(null).getExtensions().getByName(IndexManager.EXTENSION_NAME);
if (oakIndexDefsExtension == null) {
oakIndexDefsExtension = new Extension(ExtensionType.JSON, IndexManager.EXTENSION_NAME, ExtensionState.OPTIONAL);
getRunMode(null).getExtensions().add(oakIndexDefsExtension);
oakIndexDefsExtension.setJSON(text);
} else {
oakIndexDefsExtension.setJSON(oakIndexDefsExtension.getText().concat(System.lineSeparator()).concat(text));
}
}
private static void checkReferences(@NotNull final Dictionary<String, Object> configurationProperties, @NotNull final String pid) throws ConverterException {
final String[] references = Converters.standardConverter().convert(configurationProperties.get("references")).to(String[].class);
if (references != null && references.length > 0) {
for (final String r : references) {
if (r != null && !r.trim().isEmpty()) {
throw new ConverterException("References are not supported for repoinit (configuration " + pid + ")");
}
}
}
}
@Override
public void start() {
// nothing to do
}
@Override
public void end() {
// nothing to do
}
@Override
public void startPackage(@NotNull VaultPackage originalPackage) {
packageIds.push(originalPackage.getId().toString());
}
@Override
public void endPackage(@NotNull PackageId originalPackageId, @NotNull VaultPackage convertedPackage) {
packageIds.pop();
}
@Override
public void startSubPackage(@NotNull String path, @NotNull VaultPackage originalPackage) {
packageIds.push(originalPackage.getId().toString());
}
@Override
public void endSubPackage(@NotNull String path, @NotNull PackageId originalPackageId, @NotNull VaultPackage convertedPackage) {
packageIds.pop();
}
}