| /* |
| * 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 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; |
| |
| 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.KeyValueMap; |
| 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; |
| |
| /** 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 KeyValueMap variables, |
| final Bundles bundles, |
| final Configurations configurations, |
| final Extensions extensions, |
| final KeyValueMap 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); |
| } |
| } |
| } |