/* * 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.provisioning.model; import static org.apache.sling.provisioning.model.ModelResolveUtility.resolveArtifactVersion; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import org.apache.sling.provisioning.model.MergeUtility.MergeOptions; /** * Model utility */ public abstract class ModelUtility { /** * Merge the additional model into the base model. * @param base The base model. * @param additional The additional model. * @deprecated Use {link {@link MergeUtility#merge(Model, Model)} */ @Deprecated public static void merge(final Model base, final Model additional) { MergeUtility.merge(base, additional); } /** * Merge the additional model into the base model. * @param base The base model. * @param additional The additional model. * @param handleRemove Handle special remove run mode * @since 1.2 * @deprecated Use {link {@link MergeUtility#merge(Model, Model, org.apache.sling.provisioning.model.MergeUtility.MergeOptions)} */ @Deprecated public static void merge(final Model base, final Model additional, final boolean handleRemove) { final MergeOptions opts = new MergeOptions(); opts.setHandleRemoveRunMode(handleRemove); MergeUtility.merge(base, additional, opts); } /** * Optional variable resolver */ public interface VariableResolver { /** * Resolve the variable. * An implementation might get the value of a variable from the system properties, * or the environment etc. * As a fallback, the resolver should check the variables of the feature. * @param feature The feature * @param name The variable name * @return The variable value or null. */ String resolve(final Feature feature, final String name); } /** * Optional artifact dependency version resolver */ public interface ArtifactVersionResolver { /** * Setting a version for an artifact dependency in a Sling Provisioning file is optional. * By default an artifact without a defined version gets "LATEST" as version. * By defining an DependencyVersionResolver it is possible to plugin in an external dependency resolver * which decides which version to use if no version is given in the provisioning file. * If an exact version is given in the provisioning file this is always used. * @param artifact Artifact without version (version is set to LATEST) * @return New version, or null if the version should not be changed */ String resolve(final Artifact artifact); } /** * Parameter builder class for {@link ModelUtility#getEffectiveModel(Model, ResolverOptions)} method. */ public static final class ResolverOptions { private VariableResolver variableResolver; private ArtifactVersionResolver artifactVersionResolver; public VariableResolver getVariableResolver() { return variableResolver; } public ResolverOptions variableResolver(VariableResolver variableResolver) { this.variableResolver = variableResolver; return this; } public ArtifactVersionResolver getArtifactVersionResolver() { return artifactVersionResolver; } public ResolverOptions artifactVersionResolver(ArtifactVersionResolver dependencyVersionResolver) { this.artifactVersionResolver = dependencyVersionResolver; return this; } } /** * Replace all variables in the model and return a new model with the replaced values. * @param model The base model. * @param resolver Optional variable resolver. * @return The model with replaced variables. * @throws IllegalArgumentException If a variable can't be replaced or configuration properties can't be parsed * @deprecated Use {@link #getEffectiveModel(Model)} or {@link #getEffectiveModel(Model, ResolverOptions)} instead */ @Deprecated public static Model getEffectiveModel(final Model model, final VariableResolver resolver) { return getEffectiveModel(model, new ResolverOptions().variableResolver(resolver)); } /** * Replace all variables in the model and return a new model with the replaced values. * @param model The base model. * @return The model with replaced variables. * @throws IllegalArgumentException If a variable can't be replaced or configuration properties can't be parsed * @since 1.3 */ public static Model getEffectiveModel(final Model model) { return getEffectiveModel(model, new ResolverOptions()); } /** * Replace all variables in the model and return a new model with the replaced values. * @param model The base model. * @param options Resolver options. * @return The model with replaced variables. * @throws IllegalArgumentException If a variable can't be replaced or configuration properties can't be parsed * @since 1.3 */ public static Model getEffectiveModel(final Model model, final ResolverOptions options) { ModelProcessor processor = new EffectiveModelProcessor(options); return processor.process(model); } /** * Validates the model. * * @param model The model to validate * @return A map with errors or {@code null} if valid. */ public static Map<Traceable, String> validate(final Model model) { final Map<Traceable, String> errors = new HashMap<>(); for(final Feature feature : model.getFeatures() ) { // validate feature if ( feature.getName() == null || feature.getName().isEmpty() ) { addError(errors, feature, "Name is required for a feature."); } // version should be a valid version if ( feature.getVersion() != null ) { try { new Version(feature.getVersion()); } catch ( final IllegalArgumentException iae) { addError(errors, feature, "Version is not a valid version: " + feature.getVersion()); } } for(final RunMode runMode : feature.getRunModes()) { boolean hasRemove = false; final String[] rm = runMode.getNames(); if ( rm != null ) { int hasSpecial = 0; for(final String m : rm) { if ( m.startsWith(":") ) { if ( hasSpecial > 0 ) { if ( hasSpecial == 1 ) { if ( ModelConstants.RUN_MODE_REMOVE.equals(m) && !hasRemove) { hasRemove = true; hasSpecial = 2; } else if ( hasRemove && !ModelConstants.RUN_MODE_REMOVE.equals(m) ) { hasSpecial = 2; } else { hasSpecial = 2; addError(errors, runMode, "Invalid modes " + Arrays.toString(rm)); break; } } else { hasSpecial++; addError(errors, runMode, "Invalid modes " + Arrays.toString(rm)); break; } } else { hasSpecial = 1; hasRemove = ModelConstants.RUN_MODE_REMOVE.equals(m); } } } } for(final ArtifactGroup sl : runMode.getArtifactGroups()) { if ( sl.getStartLevel() < 0 ) { addError(errors, sl, "Invalid start level " + sl.getStartLevel()); } for(final Artifact a : sl) { String error = null; if ( a.getGroupId() == null || a.getGroupId().isEmpty() ) { error = "groupId missing"; } if ( a.getArtifactId() == null || a.getArtifactId().isEmpty() ) { error = (error != null ? error + ", " : "") + "artifactId missing"; } if ( a.getVersion() == null || a.getVersion().isEmpty() ) { error = (error != null ? error + ", " : "") + "version missing"; } if ( a.getType() == null || a.getType().isEmpty() ) { error = (error != null ? error + ", " : "") + "type missing"; } if (error != null) { addError(errors, a, error); } } } for(final Configuration c : runMode.getConfigurations()) { String error = null; if ( c.getPid() == null || c.getPid().isEmpty() ) { error = "pid missing"; } if ( c.isSpecial() && c.getFactoryPid() != null ) { error = (error != null ? error + ", " : "") + "factory pid not allowed for special configuration"; } if ( c.getProperties().isEmpty() && !hasRemove ) { error = (error != null ? error + ", " : "") + "configuration properties missing"; } if (error != null) { addError(errors, c, error); } } } } if ( errors.isEmpty()) { return null; } return errors; } /** * Applies a set of variables to the given model. * All variables that are referenced anywhere within the model are detected and passed to the given variable resolver. * The variable resolver may look up variables on it's own, or fall back to the variables already defined for the feature. * All resolved variable values are collected and put to the "variables" section of the resulting model. * @param model Original model * @param resolver Variable resolver * @return Model with updated "variables" section. * @throws IllegalArgumentException If a variable can't be replaced or configuration properties can't be parsed * @since 1.3 */ public static Model applyVariables(final Model model, final VariableResolver resolver) { // define delegating resolver that collects all variable names and value per feature final Map<String,Map<String,String>> collectedVars = new HashMap<>(); VariableResolver variableCollector = new VariableResolver() { @Override public String resolve(Feature feature, String name) { String value = resolver.resolve(feature, name); if (value != null) { Map<String,String> featureVars = collectedVars.get(feature.getName()); if (featureVars == null) { featureVars = new HashMap<>(); collectedVars.put(feature.getName(), featureVars); } featureVars.put(name, value); } return value; } }; // use effective model processor to collect variables, but drop the resulting model new EffectiveModelProcessor(new ResolverOptions().variableResolver(variableCollector)).process(model); // define a processor that updates the "variables" sections in the features ModelProcessor variablesUpdater = new ModelProcessor() { @Override protected KeyValueMap<String> processVariables(KeyValueMap<String> variables, Feature newFeature) { KeyValueMap<String> newVariables = new KeyValueMap<>(); Map<String,String> featureVars = collectedVars.get(newFeature.getName()); if (featureVars != null) { for (Map.Entry<String, String> entry : featureVars.entrySet()) { newVariables.put(entry.getKey(), entry.getValue()); } } return newVariables; } }; // return model with replaced "variables" sections return variablesUpdater.process(model); } /** * Resolves artifact versions that are no set explicitly in the provisioning file via the given resolver (version = "LATEST"). * If the resolver does not resolve to a version "LATEST" is left in the model. * The resolver may decide to raise an IllegalArgumentException in this case if unresolved dependencies are no allowed. * @param model Original model * @param resolver Artifact version resolver * @return Model with updated artifact versions * @throws IllegalArgumentException If the provider does not allow unresolved version and a version could not be resolved * @since 1.3 */ public static Model applyArtifactVersions(final Model model, final ArtifactVersionResolver resolver) { // define a processor that updates the versions of artifacts ModelProcessor versionUpdater = new ModelProcessor() { @Override protected Artifact processArtifact(Artifact artifact, Feature newFeature, RunMode newRunMode) { String newVersion = resolveArtifactVersion( artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(), artifact.getClassifier(), artifact.getType(), resolver); return new Artifact(artifact.getGroupId(), artifact.getArtifactId(), newVersion, artifact.getClassifier(), artifact.getType(), artifact.getMetadata()); } }; // return model with updated version artifacts return versionUpdater.process(model); } /** * Validates the model and checks that each feature has a valid version. * * This method first calls {@link #validate(Model)} and then checks * that each feature has a version. * * @param model The model to validate * @return A map with errors or {@code null} if valid. * @since 1.9 */ public static Map<Traceable, String> validateIncludingVersion(final Model model) { Map<Traceable, String> errors = validate(model); for(final Feature feature : model.getFeatures()) { if ( feature.getVersion() == null ) { if ( errors == null ) { errors = new HashMap<>(); } addError(errors, feature, "Feature must have a version."); } } return errors; } /** * Add an error for the {@code Traceable} to the error map * @param errors The map of errors * @param object The traceable object * @param error The error message * @since 1.9 */ private static void addError(final Map<Traceable, String> errors, final Traceable object, final String error) { String value = errors.get(object); errors.put(object, (value == null ? error : value + " " + error)); } }