blob: 4946cbf41715b33f403dcc2b33f553bf0c2dfbf4 [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.provisioning.model;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.felix.cm.file.ConfigurationHandler;
/**
* Merge two models
*/
public abstract class ModelUtility {
/**
* Merge the additional model into the base model.
* @param base The base model.
* @param additional The additional model.
*/
public static void merge(final Model base, final Model additional) {
// features
for(final Feature feature : additional.getFeatures()) {
final Feature baseFeature = base.getOrCreateFeature(feature.getName());
// variables
baseFeature.getVariables().putAll(feature.getVariables());
// run modes
for(final RunMode runMode : feature.getRunModes()) {
// check for special remove run mode
String names[] = runMode.getNames();
if ( names != null ) {
int removeIndex = -1;
int index = 0;
for(final String name : names) {
if ( name.equals(ModelConstants.RUN_MODE_REMOVE) ) {
removeIndex = index;
break;
}
index++;
}
if ( removeIndex != -1 ) {
String[] newNames = null;
if ( names.length > 1 ) {
newNames = new String[names.length - 1];
index = 0;
for(final String name : names) {
if ( !name.equals(ModelConstants.RUN_MODE_REMOVE) ) {
newNames[index++] = name;
}
}
}
names = newNames;
final RunMode baseRunMode = baseFeature.getRunMode(names);
if ( baseRunMode != null ) {
// artifact groups
for(final ArtifactGroup group : runMode.getArtifactGroups()) {
for(final Artifact artifact : group) {
for(final ArtifactGroup searchGroup : baseRunMode.getArtifactGroups()) {
final Artifact found = searchGroup.search(artifact);
if ( found != null ) {
searchGroup.remove(found);
}
}
}
}
// configurations
for(final Configuration config : runMode.getConfigurations()) {
final Configuration found = baseRunMode.getConfiguration(config.getPid(), config.getFactoryPid());
if ( found != null ) {
baseRunMode.getConfigurations().remove(found);
}
}
// settings
for(final Map.Entry<String, String> entry : runMode.getSettings() ) {
baseRunMode.getSettings().remove(entry.getKey());
}
}
continue;
}
}
final RunMode baseRunMode = baseFeature.getOrCreateRunMode(names);
// artifact groups
for(final ArtifactGroup group : runMode.getArtifactGroups()) {
final ArtifactGroup baseGroup = baseRunMode.getOrCreateArtifactGroup(group.getStartLevel());
for(final Artifact artifact : group) {
for(final ArtifactGroup searchGroup : baseRunMode.getArtifactGroups()) {
final Artifact found = searchGroup.search(artifact);
if ( found != null ) {
searchGroup.remove(found);
}
}
baseGroup.add(artifact);
}
}
// configurations
for(final Configuration config : runMode.getConfigurations()) {
final Configuration found = baseRunMode.getOrCreateConfiguration(config.getPid(), config.getFactoryPid());
mergeConfiguration(found, config);
}
// settings
for(final Map.Entry<String, String> entry : runMode.getSettings() ) {
baseRunMode.getSettings().put(entry.getKey(), entry.getValue());
}
}
}
}
/**
* Merge two configurations
* @param baseConfig The base configuration.
* @param mergeConfig The merge configuration.
*/
private static void mergeConfiguration(final Configuration baseConfig, final Configuration mergeConfig) {
// check for merge mode
final boolean isNew = baseConfig.getProperties().isEmpty();
if ( isNew ) {
copyConfigurationProperties(baseConfig, mergeConfig);
final Object mode = mergeConfig.getProperties().get(ModelConstants.CFG_UNPROCESSED_MODE);
if ( mode != null ) {
baseConfig.getProperties().put(ModelConstants.CFG_UNPROCESSED_MODE, mode);
}
} else {
final boolean baseIsRaw = baseConfig.getProperties().get(ModelConstants.CFG_UNPROCESSED) != null;
final boolean mergeIsRaw = mergeConfig.getProperties().get(ModelConstants.CFG_UNPROCESSED) != null;
// simplest case, both are raw
if ( baseIsRaw && mergeIsRaw ) {
final String cfgMode = (String)mergeConfig.getProperties().get(ModelConstants.CFG_UNPROCESSED_MODE);
if ( cfgMode == null || ModelConstants.CFG_MODE_OVERWRITE.equals(cfgMode) ) {
copyConfigurationProperties(baseConfig, mergeConfig);
} else {
final Configuration newConfig = new Configuration(baseConfig.getPid(), baseConfig.getFactoryPid());
getProcessedConfiguration(newConfig, baseConfig);
clearConfiguration(baseConfig);
copyConfigurationProperties(baseConfig, newConfig);
clearConfiguration(newConfig);
getProcessedConfiguration(newConfig, mergeConfig);
if ( baseConfig.isSpecial() ) {
final String baseValue = baseConfig.getProperties().get(baseConfig.getPid()).toString();
final String mergeValue = newConfig.getProperties().get(baseConfig.getPid()).toString();
baseConfig.getProperties().put(baseConfig.getPid(), baseValue + "\n" + mergeValue);
} else {
copyConfigurationProperties(baseConfig, newConfig);
}
}
// another simple case, both are not raw
} else if ( !baseIsRaw && !mergeIsRaw ) {
// merge mode is always overwrite
clearConfiguration(baseConfig);
copyConfigurationProperties(baseConfig, mergeConfig);
// base is not raw but merge is
} else if ( !baseIsRaw && mergeIsRaw ) {
final String cfgMode = (String)mergeConfig.getProperties().get(ModelConstants.CFG_UNPROCESSED_MODE);
if ( cfgMode == null || ModelConstants.CFG_MODE_OVERWRITE.equals(cfgMode) ) {
clearConfiguration(baseConfig);
copyConfigurationProperties(baseConfig, mergeConfig);
} else {
final Configuration newMergeConfig = new Configuration(mergeConfig.getPid(), mergeConfig.getFactoryPid());
getProcessedConfiguration(newMergeConfig, mergeConfig);
if ( baseConfig.isSpecial() ) {
final String baseValue = baseConfig.getProperties().get(baseConfig.getPid()).toString();
final String mergeValue = newMergeConfig.getProperties().get(baseConfig.getPid()).toString();
baseConfig.getProperties().put(baseConfig.getPid(), baseValue + "\n" + mergeValue);
} else {
copyConfigurationProperties(baseConfig, newMergeConfig);
}
}
// base is raw, but merge is not raw
} else {
// merge mode is always overwrite
clearConfiguration(baseConfig);
copyConfigurationProperties(baseConfig, mergeConfig);
}
}
}
private static void clearConfiguration(final Configuration cfg) {
final Set<String> keys = new HashSet<String>();
final Enumeration<String> e = cfg.getProperties().keys();
while ( e.hasMoreElements() ) {
keys.add(e.nextElement());
}
for(final String key : keys) {
cfg.getProperties().remove(key);
}
}
private static void copyConfigurationProperties(final Configuration baseConfig, final Configuration mergeConfig) {
final Enumeration<String> e = mergeConfig.getProperties().keys();
while ( e.hasMoreElements() ) {
final String key = e.nextElement();
if ( !key.equals(ModelConstants.CFG_UNPROCESSED_MODE) ) {
baseConfig.getProperties().put(key, mergeConfig.getProperties().get(key));
}
}
}
/**
* 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);
}
/**
* 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
*/
public static Model getEffectiveModel(final Model model, final VariableResolver resolver) {
final Model result = new Model();
result.setLocation(model.getLocation());
for(final Feature feature : model.getFeatures()) {
final Feature newFeature = result.getOrCreateFeature(feature.getName());
newFeature.setComment(feature.getComment());
newFeature.setLocation(feature.getLocation());
newFeature.getVariables().setComment(feature.getVariables().getComment());
newFeature.getVariables().setLocation(feature.getVariables().getLocation());
newFeature.getVariables().putAll(feature.getVariables());
for(final RunMode runMode : feature.getRunModes()) {
final RunMode newRunMode = newFeature.getOrCreateRunMode(runMode.getNames());
newRunMode.setLocation(runMode.getLocation());
for(final ArtifactGroup group : runMode.getArtifactGroups()) {
final ArtifactGroup newGroup = newRunMode.getOrCreateArtifactGroup(group.getStartLevel());
newGroup.setComment(group.getComment());
newGroup.setLocation(group.getLocation());
for(final Artifact artifact : group) {
final Artifact newArtifact = new Artifact(replace(feature, artifact.getGroupId(), resolver),
replace(feature, artifact.getArtifactId(), resolver),
replace(feature, artifact.getVersion(), resolver),
replace(feature, artifact.getClassifier(), resolver),
replace(feature, artifact.getType(), resolver));
newArtifact.setComment(artifact.getComment());
newArtifact.setLocation(artifact.getLocation());
newGroup.add(newArtifact);
}
}
newRunMode.getConfigurations().setComment(runMode.getConfigurations().getComment());
newRunMode.getConfigurations().setLocation(runMode.getConfigurations().getLocation());
for(final Configuration config : runMode.getConfigurations()) {
final Configuration newConfig = newRunMode.getOrCreateConfiguration(config.getPid(), config.getFactoryPid());
getProcessedConfiguration(newConfig, config);
}
newRunMode.getSettings().setComment(runMode.getSettings().getComment());
newRunMode.getSettings().setLocation(runMode.getSettings().getLocation());
for(final Map.Entry<String, String> entry : runMode.getSettings() ) {
newRunMode.getSettings().put(entry.getKey(), replace(feature, entry.getValue(),
new VariableResolver() {
@Override
public String resolve(final Feature feature, final String name) {
if ( "sling.home".equals(name) ) {
return "${sling.home}";
}
if ( resolver != null ) {
return resolver.resolve(feature, name);
}
return feature.getVariables().get(name);
}
}));
}
}
}
return result;
}
/**
* Replace properties in the string.
*
* @param feature The feature
* @param v The variable name
* @param resolver Optional resolver
* @result The value of the variable
* @throws IllegalArgumentException If variable can't be found.
*/
static String replace(final Feature feature, final String v, final VariableResolver resolver) {
if ( v == null ) {
return null;
}
String msg = v;
// check for variables
int pos = -1;
int start = 0;
while ( ( pos = msg.indexOf('$', start) ) != -1 ) {
boolean escapedVariable = (pos > 0 && msg.charAt(pos - 1) == '\\');
if ( msg.length() > pos && msg.charAt(pos + 1) == '{' && (pos == 0 || msg.charAt(pos - 1) != '$') ) {
final int endPos = msg.indexOf('}', pos);
if ( endPos != -1 ) {
final String name = msg.substring(pos + 2, endPos);
final String value;
if (escapedVariable) {
value = "\\${" + name + "}";
} else if ( resolver != null ) {
value = resolver.resolve(feature, name);
} else {
value = feature.getVariables().get(name);
}
if ( value == null ) {
throw new IllegalArgumentException("Unknown variable: " + name);
}
int startPos = escapedVariable ? pos - 1 : pos;
msg = msg.substring(0, startPos) + value + msg.substring(endPos + 1);
}
}
start = pos + 1;
}
return msg;
}
/**
* Validates the model.
* @param model The model to validate
* @return A map with errors or {@code null}.
*/
public static Map<Traceable, String> validate(final Model model) {
final Map<Traceable, String> errors = new HashMap<Traceable, String>();
for(final Feature feature : model.getFeatures() ) {
// validate feature
if ( feature.getName() == null || feature.getName().isEmpty() ) {
errors.put(feature, "Name is required for a feature.");
}
for(final RunMode runMode : feature.getRunModes()) {
final String[] rm = runMode.getNames();
if ( rm != null ) {
boolean hasSpecial = false;
for(final String m : rm) {
if ( m.startsWith(":") ) {
if ( hasSpecial ) {
errors.put(runMode, "Invalid modes " + Arrays.toString(rm));
break;
}
hasSpecial = true;
}
}
}
for(final ArtifactGroup sl : runMode.getArtifactGroups()) {
if ( sl.getStartLevel() < 0 ) {
errors.put(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) {
errors.put(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() ) {
error = (error != null ? error + ", " : "") + "configuration properties missing";
}
if (error != null) {
errors.put(c, error);
}
}
}
}
if ( errors.size() == 0 ) {
return null;
}
return errors;
}
private static void getProcessedConfiguration(final Configuration newConfig, final Configuration config) {
newConfig.setComment(config.getComment());
newConfig.setLocation(config.getLocation());
// check for raw configuration
final String rawConfig = (String)config.getProperties().get(ModelConstants.CFG_UNPROCESSED);
if ( rawConfig != null ) {
if ( config.isSpecial() ) {
newConfig.getProperties().put(config.getPid(), rawConfig);
} else {
final String format = (String)config.getProperties().get(ModelConstants.CFG_UNPROCESSED_FORMAT);
if ( ModelConstants.CFG_FORMAT_PROPERTIES.equals(format) ) {
// properties
final Properties props = new Properties();
try {
props.load(new StringReader(rawConfig));
} catch ( final IOException ioe) {
throw new IllegalArgumentException("Unable to read configuration properties.", ioe);
}
final Enumeration<Object> i = props.keys();
while ( i.hasMoreElements() ) {
final String key = (String)i.nextElement();
newConfig.getProperties().put(key, props.get(key));
}
} else {
// Apache Felix CA format
// the raw format might have comments, we have to remove them first
final StringBuilder sb = new StringBuilder();
try {
final LineNumberReader lnr = new LineNumberReader(new StringReader(rawConfig));
String line = null;
while ((line = lnr.readLine()) != null ) {
line = line.trim();
if ( line.isEmpty() || line.startsWith("#")) {
continue;
}
sb.append(line);
sb.append('\n');
}
} catch ( final IOException ioe) {
throw new IllegalArgumentException("Unable to read configuration properties: " + config, ioe);
}
ByteArrayInputStream bais = null;
try {
bais = new ByteArrayInputStream(sb.toString().getBytes("UTF-8"));
@SuppressWarnings("unchecked")
final Dictionary<String, Object> props = ConfigurationHandler.read(bais);
final Enumeration<String> i = props.keys();
while ( i.hasMoreElements() ) {
final String key = i.nextElement();
newConfig.getProperties().put(key, props.get(key));
}
} catch ( final IOException ioe) {
throw new IllegalArgumentException("Unable to read configuration properties: " + config, ioe);
} finally {
if ( bais != null ) {
try {
bais.close();
} catch ( final IOException ignore ) {
// ignore
}
}
}
}
}
} else {
// simply copy
final Enumeration<String> i = config.getProperties().keys();
while ( i.hasMoreElements() ) {
final String key = i.nextElement();
newConfig.getProperties().put(key, config.getProperties().get(key));
}
}
}
}