blob: 148bcc619e875869a1dd1cc74d9cf0e51b6ec8de [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.modelconverter;
import org.apache.sling.feature.ArtifactId;
import org.apache.sling.feature.Bundles;
import org.apache.sling.feature.Configurations;
import org.apache.sling.feature.Extension;
import org.apache.sling.feature.ExtensionType;
import org.apache.sling.feature.Extensions;
import org.apache.sling.feature.FeatureConstants;
import org.apache.sling.feature.io.file.ArtifactHandler;
import org.apache.sling.feature.io.file.ArtifactManager;
import org.apache.sling.feature.io.file.ArtifactManagerConfig;
import org.apache.sling.feature.io.json.FeatureJSONWriter;
import org.apache.sling.provisioning.model.Artifact;
import org.apache.sling.provisioning.model.ArtifactGroup;
import org.apache.sling.provisioning.model.Configuration;
import org.apache.sling.provisioning.model.Feature;
import org.apache.sling.provisioning.model.MergeUtility;
import org.apache.sling.provisioning.model.Model;
import org.apache.sling.provisioning.model.ModelConstants;
import org.apache.sling.provisioning.model.ModelUtility;
import org.apache.sling.provisioning.model.ModelUtility.ResolverOptions;
import org.apache.sling.provisioning.model.ModelUtility.VariableResolver;
import org.apache.sling.provisioning.model.RunMode;
import org.apache.sling.provisioning.model.Section;
import org.apache.sling.provisioning.model.Traceable;
import org.apache.sling.provisioning.model.io.ModelReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/** Converter that converts the provisioning model to the feature model.
*/
public class ProvisioningToFeature {
private static Logger LOGGER = LoggerFactory.getLogger(ProvisioningToFeature.class);
public static List<File> convert(File file, File outDir, Map<String, Object> options) {
Model model = createModel(Collections.singletonList(file), null, true, false);
String bareFileName = getBareFileName(file);
final List<org.apache.sling.feature.Feature> features = buildFeatures(model, bareFileName, options);
List<File> files = new ArrayList<>();
for (org.apache.sling.feature.Feature f : features) {
String id = f.getVariables().get(FeatureToProvisioning.PROVISIONING_MODEL_NAME_VARIABLE);
if (id == null) {
id = f.getId().getArtifactId();
}
id = id.replaceAll("[:]","");
id = bareFileName + "_" + id;
File outFile = new File(outDir, id + ".json");
files.add(outFile);
if (outFile.exists()) {
if (outFile.lastModified() > file.lastModified()) {
LOGGER.debug("Skipping the generation of {} as this file already exists and is newer.", outFile);
continue;
} else {
LOGGER.debug("Deleting existing file {} as source is newer", outFile);
outFile.delete();
}
}
writeFeature(f, outFile.getAbsolutePath(), 0);
}
return files;
}
public static void convert(List<File> files, String outputFile, String runModes, boolean createApp,
boolean includeModelInfo, String propsFile) {
final Model model = createModel(files, runModes, false, includeModelInfo);
final List<org.apache.sling.feature.Feature> features = buildFeatures(model, null, Collections.emptyMap());
int index = 1;
for(final org.apache.sling.feature.Feature feature : features) {
writeFeature(feature, outputFile, features.size() > 1 ? index : 0);
index++;
}
}
/**
* Read the models and prepare the model
* @param files The model files
* @param includeModelInfo
*/
private static Model createModel(final List<File> files,
final String runModes, boolean allRunModes, boolean includeModelInfo) {
LOGGER.info("Assembling model...");
ResolverOptions variableResolver = new ResolverOptions().variableResolver(new VariableResolver() {
@Override
public String resolve(final Feature feature, final String name) {
// Keep variables as-is in the model
return "${" + name + "}";
}
});
Model model = null;
for(final File initFile : files) {
try {
model = processModel(model, initFile, includeModelInfo, variableResolver);
} catch ( final IOException iae) {
LOGGER.error("Unable to read provisioning model {} : {}", initFile, iae.getMessage(), iae);
System.exit(1);
}
}
final Model effectiveModel = ModelUtility.getEffectiveModel(model, variableResolver);
final Map<Traceable, String> errors = ModelUtility.validate(effectiveModel);
if ( errors != null ) {
LOGGER.error("Invalid assembled provisioning model.");
for(final Map.Entry<Traceable, String> entry : errors.entrySet()) {
LOGGER.error("- {} : {}", entry.getKey().getLocation(), entry.getValue());
}
System.exit(1);
}
final Set<String> modes;
if (allRunModes) {
modes = new HashSet<>();
for (Feature f : effectiveModel.getFeatures()) {
for (RunMode rm : f.getRunModes()) {
String[] names = rm.getNames();
if (names != null) {
modes.addAll(Arrays.asList(names));
}
}
}
} else {
modes = calculateRunModes(effectiveModel, runModes);
}
return effectiveModel;
}
/**
* Process the given model and merge it into the provided model
* @param model The already read model
* @param modelFile The model file
* @param includeModelInfo
* @return The merged model
* @throws IOException If reading fails
*/
private static Model processModel(Model model,
File modelFile, boolean includeModelInfo) throws IOException {
return processModel(model, modelFile, includeModelInfo,
new ResolverOptions().variableResolver(new VariableResolver() {
@Override
public String resolve(final Feature feature, final String name) {
return name;
}
})
);
}
private static Model processModel(Model model,
File modelFile, boolean includeModelInfo, ResolverOptions options) throws IOException {
LOGGER.info("- reading model {}", modelFile);
final Model nextModel = readProvisioningModel(modelFile);
final Model effectiveModel = ModelUtility.getEffectiveModel(nextModel, options);
for(final Feature feature : effectiveModel.getFeatures()) {
for(final RunMode runMode : feature.getRunModes()) {
for(final ArtifactGroup group : runMode.getArtifactGroups()) {
final List<org.apache.sling.provisioning.model.Artifact> removeList = new ArrayList<>();
for(final org.apache.sling.provisioning.model.Artifact a : group) {
if ( "slingstart".equals(a.getType())
|| "slingfeature".equals(a.getType())) {
final ArtifactManagerConfig cfg = new ArtifactManagerConfig();
final ArtifactManager mgr = ArtifactManager.getArtifactManager(cfg);
final ArtifactId correctedId = new ArtifactId(a.getGroupId(),
a.getArtifactId(),
a.getVersion(),
"slingstart".equals(a.getType()) ? "slingfeature" : a.getClassifier(),
"txt");
final ArtifactHandler handler = mgr.getArtifactHandler(correctedId.toMvnUrl());
model = processModel(model, handler.getFile(), includeModelInfo);
removeList.add(a);
} else {
final org.apache.sling.provisioning.model.Artifact realArtifact = nextModel.getFeature(feature.getName()).getRunMode(runMode.getNames()).getArtifactGroup(group.getStartLevel()).search(a);
if ( includeModelInfo ) {
realArtifact.getMetadata().put("model-filename", modelFile.getName());
}
if ( runMode.getNames() != null ) {
realArtifact.getMetadata().put("runmodes", String.join(",", runMode.getNames()));
}
}
}
for(final org.apache.sling.provisioning.model.Artifact r : removeList) {
nextModel.getFeature(feature.getName()).getRunMode(runMode.getNames()).getArtifactGroup(group.getStartLevel()).remove(r);
}
}
}
}
if ( model == null ) {
model = nextModel;
} else {
MergeUtility.merge(model, nextModel);
}
return model;
}
/**
* Read the provisioning model
*/
private static Model readProvisioningModel(final File file)
throws IOException {
try (final FileReader is = new FileReader(file)) {
return ModelReader.read(is, file.getAbsolutePath());
}
}
private static Set<String> calculateRunModes(final Model model, final String runModes) {
final Set<String> modesSet = new HashSet<>();
// check configuration property first
if (runModes != null && runModes.trim().length() > 0) {
final String[] modes = runModes.split(",");
for(int i=0; i < modes.length; i++) {
modesSet.add(modes[i].trim());
}
}
// handle configured options
final Feature feature = model.getFeature(ModelConstants.FEATURE_BOOT);
if ( feature != null ) {
handleOptions(modesSet, feature.getRunMode().getSettings().get("sling.run.mode.options"));
handleOptions(modesSet, feature.getRunMode().getSettings().get("sling.run.mode.install.options"));
}
return modesSet;
}
private static void handleOptions(final Set<String> modesSet, final String propOptions) {
if ( propOptions != null && propOptions.trim().length() > 0 ) {
final String[] options = propOptions.trim().split("\\|");
for(final String opt : options) {
String selected = null;
final String[] modes = opt.trim().split(",");
for(int i=0; i<modes.length; i++) {
modes[i] = modes[i].trim();
if ( selected != null ) {
modesSet.remove(modes[i]);
} else {
if ( modesSet.contains(modes[i]) ) {
selected = modes[i];
}
}
}
if ( selected == null ) {
selected = modes[0];
modesSet.add(modes[0]);
}
}
}
}
private static void buildFromFeature(final Feature feature,
final Map<String,String> variables,
final Bundles bundles,
final Configurations configurations,
final Extensions extensions,
final Map<String,String> properties) {
for (Iterator<Map.Entry<String, String>> it = feature.getVariables().iterator(); it.hasNext(); ) {
Entry<String, String> entry = it.next();
variables.put(entry.getKey(), entry.getValue());
}
Extension cpExtension = extensions.getByName(FeatureConstants.EXTENSION_NAME_CONTENT_PACKAGES);
for(final RunMode runMode : feature.getRunModes() ) {
for(final ArtifactGroup group : runMode.getArtifactGroups()) {
for(final Artifact artifact : group) {
final ArtifactId id = ArtifactId.fromMvnUrl(artifact.toMvnUrl());
final org.apache.sling.feature.Artifact newArtifact = new org.apache.sling.feature.Artifact(id);
for(final Map.Entry<String, String> entry : artifact.getMetadata().entrySet()) {
newArtifact.getMetadata().put(entry.getKey(), entry.getValue());
}
if ( newArtifact.getId().getType().equals("zip") ) {
if ( cpExtension == null ) {
cpExtension = new Extension(ExtensionType.ARTIFACTS, FeatureConstants.EXTENSION_NAME_CONTENT_PACKAGES, true);
extensions.add(cpExtension);
}
cpExtension.getArtifacts().add(newArtifact);
} else {
int startLevel = group.getStartLevel();
if ( startLevel == 0) {
if ( ModelConstants.FEATURE_BOOT.equals(feature.getName()) ) {
startLevel = 1;
} else if ( startLevel == 0 ) {
startLevel = 20;
}
}
newArtifact.getMetadata().put("start-level", String.valueOf(startLevel));
bundles.add(newArtifact);
}
}
}
for(final Configuration cfg : runMode.getConfigurations()) {
String pid = cfg.getPid();
if (pid.startsWith(":")) {
// The configurator doesn't accept colons ':' in it's keys, so replace these
pid = ".." + pid.substring(1);
}
final String[] runModeNames = runMode.getNames();
if (runModeNames != null) {
pid = pid + ".runmodes." + String.join(".", runModeNames);
pid = pid.replaceAll("[:]", "..");
}
final org.apache.sling.feature.Configuration newCfg;
if ( cfg.getFactoryPid() != null ) {
newCfg = new org.apache.sling.feature.Configuration(cfg.getFactoryPid(), pid);
} else {
newCfg = new org.apache.sling.feature.Configuration(pid);
}
final Enumeration<String> keys = cfg.getProperties().keys();
while ( keys.hasMoreElements() ) {
String key = keys.nextElement();
Object value = cfg.getProperties().get(key);
if (key.startsWith(":")) {
key = ".." + key.substring(1);
}
newCfg.getProperties().put(key, value);
}
configurations.add(newCfg);
}
for(final Map.Entry<String, String> prop : runMode.getSettings()) {
String[] runModeNames = runMode.getNames();
if (runModeNames == null) {
properties.put(prop.getKey(), prop.getValue());
} else {
properties.put(prop.getKey() + ".runmodes:" + String.join(",", runModeNames),
prop.getValue());
}
}
}
final StringBuilder repoinitText = new StringBuilder();
for(final Section sect : feature.getAdditionalSections("repoinit")) {
repoinitText.append(sect.getContents()).append("\n");
}
if(repoinitText.length() > 0) {
Extension repoExtension = extensions.getByName(FeatureConstants.EXTENSION_NAME_REPOINIT);
if ( repoExtension == null ) {
repoExtension = new Extension(ExtensionType.JSON, FeatureConstants.EXTENSION_NAME_REPOINIT, true);
extensions.add(repoExtension);
repoExtension.setJSON(textToJSON(repoinitText.toString()));
} else {
throw new IllegalStateException("Repoinit sections already processed");
}
}
}
private static String textToJSON(String text) {
text = text.replace('\t', ' ');
String[] lines = text.split("[\n]");
StringBuilder sb = new StringBuilder();
sb.append('[');
boolean first = true;
for (String t : lines) {
if (first)
first = false;
else
sb.append(',');
sb.append('"');
sb.append(t);
sb.append('"');
}
sb.append(']');
return sb.toString();
}
private static List<org.apache.sling.feature.Feature> buildFeatures(Model model, String bareFileName, Map<String, Object> options) {
final List<org.apache.sling.feature.Feature> features = new ArrayList<>();
String groupId = getOption(options, "groupId", "generated");
String version = getOption(options, "version", "1.0.0");
for(final Feature feature : model.getFeatures() ) {
final String idString;
String name = feature.getName();
if ( name != null ) {
name = name.replaceAll("[:]", "");
if (!name.equals(bareFileName)) {
name = bareFileName + "_" + name;
}
if ( feature.getVersion() != null ) {
idString = groupId + "/" + name + "/" + feature.getVersion();
} else {
idString = groupId + "/" + name + "/" + version;
}
} else {
idString = groupId + "/feature/" + version;
}
final org.apache.sling.feature.Feature f = new org.apache.sling.feature.Feature(ArtifactId.parse(idString));
features.add(f);
buildFromFeature(feature, f.getVariables(), f.getBundles(), f.getConfigurations(), f.getExtensions(), f.getFrameworkProperties());
if (!f.getId().getArtifactId().equals(feature.getName())) {
f.getVariables().put(FeatureToProvisioning.PROVISIONING_MODEL_NAME_VARIABLE, feature.getName());
}
}
return features;
}
private static String getBareFileName(File file) {
String bareFileName = file.getName();
int idx = bareFileName.lastIndexOf('.');
if (idx > 0) {
bareFileName = bareFileName.substring(0, idx);
}
return bareFileName;
}
@SuppressWarnings("unchecked")
private static <T> T getOption(Map<String, Object> options, String name, T defaultValue) {
if (options.containsKey(name)) {
return (T) options.get(name);
} else {
return defaultValue;
}
}
private static void writeFeature(final org.apache.sling.feature.Feature f, String out, final int index) {
if ( index > 0 ) {
final int lastDot = out.lastIndexOf('.');
if ( lastDot == -1 ) {
out = out + "_" + String.valueOf(index);
} else {
out = out.substring(0, lastDot) + "_" + String.valueOf(index) + out.substring(lastDot);
}
}
LOGGER.info("to file {}", out);
final File file = new File(out);
while (file.exists()) {
LOGGER.error("Output file already exists: {}", file.getAbsolutePath());
System.exit(1);
}
try ( final FileWriter writer = new FileWriter(file)) {
FeatureJSONWriter.write(writer, f);
} catch ( final IOException ioe) {
LOGGER.error("Unable to write feature to {} : {}", out, ioe.getMessage(), ioe);
System.exit(1);
}
}
}