blob: 265643f589d0b0804783fbecab982ffc34dd63e7 [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 static org.apache.sling.provisioning.model.ModelResolveUtility.getProcessedConfiguration;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Utility for merging two model.
*
* @since 1.4
*/
public abstract class MergeUtility {
/**
* Options for specifying some parts of the merge operation.
*/
public static class MergeOptions {
private boolean handleRemoveRunMode = true;
private boolean latestArtifactWins = true;
/**
* Returns {@code true} if the remove run mode should be respected.
* @return {@code true} or {@code false}
*/
public boolean isHandleRemoveRunMode() {
return handleRemoveRunMode;
}
/**
* Set to {@code true} if the remove run mode should be respected.
* @param handleRemoveRunMode
*/
public void setHandleRemoveRunMode(boolean handleRemoveRunMode) {
this.handleRemoveRunMode = handleRemoveRunMode;
}
/**
* Returns {@code true} if the latest artifact should win on a merge.
* @return {@code true} or {@code false} if the artifact with the
* highest version should win
*/
public boolean isLatestArtifactWins() {
return latestArtifactWins;
}
/**
* Set to {@code true} if the latest artifact should win on a merge.
* Set to {@code false} if the artifact with the highest version should win
*/
public void setLatestArtifactWins(boolean latestArtifactWins) {
this.latestArtifactWins = latestArtifactWins;
}
}
/**
* 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) {
merge(base, additional, new MergeOptions());
}
/**
* Merge the additional model into the base model.
* <p>
* Merging is performed feature by feature. Each feature is treated separately.
* If the base model does not have a feature from the additional model, the complete
* feature is added. If the base model has a feature which is not in the additional model,
* the feature is left as is.
* <p>
* For each feature, the following actions are performed:
* <ul>
* <li>If either the base feature or the additional feature has a version, then
* the one with the higher version is used, the other one is skipped. A missing
* version is considered the lowest possible version.</li>
* <li>The feature type of the base feature is set to the type of the additional feature.</li>
* <li>All additional sections of the additional feature are added to the base feature.</li>
* <li>All variables from the additional feature are set on the base feature, overriding
* values if already present.</li>
* <li>Each run mode of the additional feature is merged into the base feature.</li>
* </ul>
* <p>
* @param base The base model.
* @param additional The additional model.
* @param options The merge options
*/
public static void merge(final Model base, final Model additional, final MergeOptions options) {
// features
for(final Feature feature : additional.getFeatures()) {
final Feature baseFeature = base.getOrCreateFeature(feature.getName());
// version check first
boolean overwrite = false;
if ( baseFeature.getVersion() != null ) {
if ( feature.getVersion() == null ) {
continue;
}
final Version baseVersion = new Version(baseFeature.getVersion());
final Version addVersion = new Version(feature.getVersion());
if ( baseVersion.compareTo(addVersion) >= 0 ) {
continue;
}
overwrite = true;
} else {
if ( feature.getVersion() != null ) {
overwrite = true;
}
}
if ( overwrite ) {
// set version
baseFeature.setVersion(feature.getVersion());
// remove everything from base feature
baseFeature.getRunModes().clear();
baseFeature.getAdditionalSections().clear();
baseFeature.getVariables().clear();
}
baseFeature.setType(feature.getType());
// additional sections
baseFeature.getAdditionalSections().addAll(feature.getAdditionalSections());
// variables
baseFeature.getVariables().putAll(feature.getVariables());
// run modes
for(final RunMode runMode : feature.getRunModes()) {
// check for special remove run mode
final String names[] = runMode.getNames();
if ( options.isHandleRemoveRunMode() && names != null ) {
if ( handleRemoveRunMode(baseFeature, runMode) ) {
continue;
}
}
final RunMode baseRunMode = baseFeature.getOrCreateRunMode(names);
// artifact groups
for(final ArtifactGroup group : runMode.getArtifactGroups()) {
final ArtifactGroup baseGroup = baseRunMode.getOrCreateArtifactGroup(group.getStartLevel());
int foundStartLevel = 0;
for(final Artifact artifact : group) {
boolean addArtifact = true;
for(final ArtifactGroup searchGroup : baseRunMode.getArtifactGroups()) {
final Artifact found = searchGroup.search(artifact);
if ( found != null ) {
if ( options.isLatestArtifactWins() ) {
searchGroup.remove(found);
foundStartLevel = searchGroup.getStartLevel();
} else {
try {
final Version baseVersion = new Version(found.getVersion());
final Version mergeVersion = new Version(artifact.getVersion());
if ( baseVersion.compareTo(mergeVersion) <= 0 ) {
searchGroup.remove(found);
foundStartLevel = searchGroup.getStartLevel();
} else {
addArtifact = false;
}
} catch ( final IllegalArgumentException iae) {
// if at least one version is not a valid maven version
if ( found.getVersion().compareTo(artifact.getVersion()) <= 0 ) {
searchGroup.remove(found);
foundStartLevel = searchGroup.getStartLevel();
} else {
addArtifact = false;
}
}
}
}
}
if ( addArtifact ) {
if ( group.getStartLevel() == 0 && foundStartLevel != 0 ) {
baseRunMode.getOrCreateArtifactGroup(foundStartLevel).add(artifact);
} else {
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());
}
}
}
}
/**
* Handle the remove run mode
* @param baseFeature The base feature
* @param runMode The current run mode
* @return {@code true} if the current run mode is a remove run mode
*/
private static boolean handleRemoveRunMode(final Feature baseFeature, final RunMode runMode) {
String names[] = runMode.getNames();
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());
}
}
return true;
}
return false;
}
/**
* 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(null, newConfig, baseConfig, false, null);
clearConfiguration(baseConfig);
copyConfigurationProperties(baseConfig, newConfig);
clearConfiguration(newConfig);
getProcessedConfiguration(null, newConfig, mergeConfig, false, null);
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(null, newMergeConfig, mergeConfig, false, null);
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));
}
}
}
}