blob: 4d4cdd170cbb2b8230c5317a10fc2d572b2543fb [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.builder;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonArrayBuilder;
import jakarta.json.JsonObject;
import jakarta.json.JsonObjectBuilder;
import jakarta.json.JsonStructure;
import jakarta.json.JsonValue;
import jakarta.json.JsonValue.ValueType;
import jakarta.json.JsonWriter;
import org.apache.sling.feature.Artifact;
import org.apache.sling.feature.ArtifactId;
import org.apache.sling.feature.Artifacts;
import org.apache.sling.feature.Configuration;
import org.apache.sling.feature.Configurations;
import org.apache.sling.feature.Extension;
import org.apache.sling.feature.Feature;
import org.apache.sling.feature.MatchingRequirement;
import org.osgi.framework.Version;
import org.osgi.resource.Capability;
/**
* Utility methods for the builders
*/
class BuilderUtil {
/** Used in override rule to have it apply to all artifacts. */
static final String CATCHALL_OVERRIDE = "*:*:";
/**
* Merge the variables
* @param targetFeature The target feature
* @param sourceFeature The source feature
* @param context Non-null map of overrides
*/
static void mergeVariables(final Feature targetFeature, final Feature sourceFeature, final Map<String,String> overrides) {
final Map<String,String> result = new LinkedHashMap<>();
final Map<String, Map<String, Object>> metadata = new HashMap<>();
// keep values from target features, no need to update origin
for (Map.Entry<String, String> entry : targetFeature.getVariables().entrySet()) {
result.put(entry.getKey(), overrides.containsKey(entry.getKey()) ? overrides.get(entry.getKey()) : entry.getValue());
metadata.put(entry.getKey(), targetFeature.getVariableMetadata(entry.getKey()));
}
// merge in from source feature
for (Map.Entry<String, String> entry : sourceFeature.getVariables().entrySet()) {
final List<ArtifactId> targetIds = (metadata.containsKey(entry.getKey())) ?
new ArrayList<>(targetFeature.getFeatureOrigins(metadata.get(entry.getKey()))) : new ArrayList<>();
targetIds.add(sourceFeature.getId());
if ( overrides.containsKey(entry.getKey()) ) {
result.put(entry.getKey(), overrides.get(entry.getKey()));
} else {
String value = entry.getValue();
if (value != null) {
String targetValue = targetFeature.getVariables().get(entry.getKey());
if (targetValue != null) {
if (!value.equals(targetValue)) {
throw new IllegalStateException(String.format("Can't merge variable '%s' defined twice (as '%s' v.s. '%s') and not overridden.", entry.getKey(), value, targetValue));
}
} else {
result.put(entry.getKey(), value);
}
} else if (!targetFeature.getVariables().containsKey(entry.getKey())) {
result.put(entry.getKey(), value);
}
}
metadata.put(entry.getKey(), new LinkedHashMap<>(sourceFeature.getVariableMetadata(entry.getKey())));
targetFeature.setFeatureOrigins(metadata.get(entry.getKey()), targetIds);
}
targetFeature.getVariables().clear();
targetFeature.getVariables().putAll(result);
for(final Map.Entry<String, Map<String, Object>> entry : metadata.entrySet()) {
targetFeature.getVariableMetadata(entry.getKey()).putAll(entry.getValue());
}
}
/**
* Merge bundles from source into target
*
* @param target The target bundles
* @param source The source bundles
* @param sourceFeature Optional, if set origin will be recorded
* @param artifactOverrides Artifact override instructions
* @param originKey An optional key used to track origins of merged
* bundles
* @throws IllegalStateException If bundles can't be merged, for example if no
* override is specified for a clash.
*/
static void mergeArtifacts(final Artifacts target,
final Artifacts source,
final Feature sourceFeature,
final List<ArtifactId> artifactOverrides,
final String originKey) {
for (final Artifact artifactFromSource : source) {
// set of artifacts in target, matching the artifact from source
// the artifacts are kept in the order of the target - hence the linked hash set.
final Set<Artifact> allExistingInTarget = new LinkedHashSet<>();
for (final ArtifactId id : artifactFromSource.getAliases(true)) {
for (Artifact targetArtifact : target) {
// Find aliased bundles in target
if (id.isSame(targetArtifact.getId())) {
allExistingInTarget.add(targetArtifact);
}
}
findAliasedArtifacts(id, target, allExistingInTarget);
}
final List<Artifact> selectedArtifacts = new ArrayList<>();
if (allExistingInTarget.isEmpty()) {
selectedArtifacts.add(artifactFromSource);
}
int insertPos = target.size();
int count = 0;
for (final Artifact existing : allExistingInTarget) {
if (sourceFeature.getId().toMvnId().equals(existing.getMetadata().get(originKey))) {
// If the source artifact came from the same feature, keep them side-by-side
// but make sure we add the target ones first - hence, the count
selectedArtifacts.add(count++, existing);
selectedArtifacts.add(artifactFromSource);
} else {
List<Artifact> artifacts = selectArtifactOverride(sourceFeature, existing, artifactFromSource, artifactOverrides, sourceFeature.getId());
// if we have an all policy we might have more then one artifact - we put the target one first
if (artifacts.size() > 1) {
selectedArtifacts.add(count++, artifacts.remove(0));
}
selectedArtifacts.addAll(artifacts);
Artifact same = null;
while ((same = target.getSame(existing.getId())) != null) {
// Keep executing removeSame() which ignores the version until last one was
// removed
final int p = target.indexOf(same);
if (p < insertPos) {
insertPos = p;
}
target.remove(p);
}
}
}
for (final Artifact sa : new LinkedHashSet<>(selectedArtifacts)) {
// create a copy to detach artifact from source
final Artifact cp = addFeatureOrigin(sa.copy(sa.getId()), sourceFeature, sa);
cp.getMetadata().put(BuilderUtil.class.getName() + "added", "true");
// Record the original feature of the bundle, if needed
if (originKey != null) {
if (sourceFeature != null && source.contains(sa) && sa.getMetadata().get(originKey) == null) {
cp.getMetadata().put(originKey, sourceFeature.getId().toMvnId());
}
}
if (insertPos == target.size()) {
target.add(cp);
insertPos = target.size();
} else {
target.add(insertPos, cp);
insertPos++;
}
}
}
for (Artifact artifact : target) {
artifact.getMetadata().remove(BuilderUtil.class.getName() + "added");
}
}
static List<Artifact> selectArtifactOverride(Artifact fromTarget, Artifact fromSource,
List<ArtifactId> artifactOverrides) {
return selectArtifactOverride(null, fromTarget, fromSource, artifactOverrides, null);
}
static List<Artifact> selectArtifactOverride(Feature source, Artifact fromTarget, Artifact fromSource,
List<ArtifactId> artifactOverrides, ArtifactId sourceID) {
if (fromTarget.getId().equals(fromSource.getId())) {
// They're the same so return the source (latest)
return Collections.singletonList(addFeatureOrigin(selectStartOrder(fromTarget, fromSource, fromSource), source, fromSource, fromTarget));
}
final Set<ArtifactId> commonPrefixes = getCommonArtifactIds(fromTarget, fromSource);
if (commonPrefixes.isEmpty()) {
throw new IllegalStateException(
"Internal error selecting override. No common prefix between " + fromTarget + " and " + fromSource);
}
final List<Artifact> result = new ArrayList<>();
if (artifactOverrides.isEmpty() && fromTarget.getMetadata().containsKey(BuilderUtil.class.getName() + "added")) {
result.add(fromTarget);
result.add(addFeatureOrigin(fromSource, source, fromSource));
return result;
}
outer: for (ArtifactId prefix : commonPrefixes) {
for (final ArtifactId override : artifactOverrides) {
if (match(prefix, override)) {
String rule = override.getVersion();
if (BuilderContext.VERSION_OVERRIDE_ALL.equalsIgnoreCase(rule)) {
result.add(fromTarget);
result.add(fromSource);
} else if (BuilderContext.VERSION_OVERRIDE_HIGHEST.equalsIgnoreCase(rule)) {
Version a1v = fromTarget.getAliases(true).stream().filter(prefix::isSame).findFirst().get().getOSGiVersion();
Version a2v = fromSource.getAliases(true).stream().filter(prefix::isSame).findFirst().get().getOSGiVersion();
result.add(
addFeatureOrigin(
selectStartOrder(fromTarget, fromSource, a1v.compareTo(a2v) > 0 ? fromTarget : fromSource),
source, fromSource, fromTarget));
} else if (BuilderContext.VERSION_OVERRIDE_FIRST.equalsIgnoreCase(rule)) {
result.add(addFeatureOrigin(selectStartOrder(fromTarget, fromSource, fromTarget), source, fromSource, fromTarget));
} else if (BuilderContext.VERSION_OVERRIDE_LATEST.equalsIgnoreCase(rule)) {
result.add(addFeatureOrigin(selectStartOrder(fromTarget, fromSource, fromSource), source, fromSource, fromTarget));
} else {
// The rule must represent a version
// See if its one of the existing artifact. If so use those, as they may have
// additional metadata
if (fromTarget.getId().getVersion().equals(rule)) {
result.add(addFeatureOrigin(selectStartOrder(fromTarget, fromSource, fromTarget), source, fromSource, fromTarget));
} else if (fromSource.getId().getVersion().equals(rule)) {
result.add(addFeatureOrigin(selectStartOrder(fromTarget, fromSource, fromSource), source, fromSource, fromTarget));
} else {
// It's a completely new artifact
result.add(addFeatureOrigin(selectStartOrder(fromTarget, fromSource, new Artifact(override)), source, fromSource, fromTarget));
}
}
break outer;
}
}
}
if (!result.isEmpty()) {
return result;
}
throw new IllegalStateException("Artifact override rule required to select between these two artifacts " +
fromTarget + " and " + fromSource + ". The rule must be specified for " + commonPrefixes);
}
private static Artifact selectStartOrder(Artifact a, Artifact b, Artifact target) {
int startOrderA = a.getStartOrder();
int startOrderB = b.getStartOrder();
int startOrderNew;
if (startOrderA == 0) {
startOrderNew = startOrderB;
} else if (startOrderB == 0) {
startOrderNew = startOrderA;
} else if (startOrderA < startOrderB) {
startOrderNew = startOrderA;
} else {
startOrderNew = startOrderB;
}
if (startOrderNew != target.getStartOrder()) {
Artifact result = target.copy(target.getId());
result.setStartOrder(startOrderNew);
return result;
}
else {
return target;
}
}
private static Artifact addFeatureOrigin(Artifact result, Feature sourceFeature, Artifact sourceArtifact, Artifact targetArtifact) {
LinkedHashSet<ArtifactId> originFeatures = new LinkedHashSet<>();
List<ArtifactId> targetOrigins = Arrays.asList(targetArtifact.getFeatureOrigins());
originFeatures.addAll(targetOrigins);
List<ArtifactId> sourceOrigings = Arrays.asList(sourceArtifact.getFeatureOrigins());
if (sourceFeature != null && sourceOrigings.isEmpty()) {
originFeatures.add(sourceFeature.getId());
}
else {
originFeatures.addAll(sourceOrigings);
}
ArtifactId[] origins = originFeatures.toArray(new ArtifactId[0]);
if (Arrays.equals(origins,result.getFeatureOrigins())) {
return result;
}
else {
result = result.copy(result.getId());
result.setFeatureOrigins(origins);
return result;
}
}
private static Artifact addFeatureOrigin(Artifact result, Feature sourceFeature, Artifact sourceArtifact) {
LinkedHashSet<ArtifactId> originFeatures = new LinkedHashSet<>();
List<ArtifactId> sourceOrigins = Arrays.asList(sourceArtifact.getFeatureOrigins());
if (sourceFeature != null && sourceOrigins.isEmpty()) {
originFeatures.add(sourceFeature.getId());
}
else if (!sourceOrigins.isEmpty()){
originFeatures.addAll(sourceOrigins);
}
ArtifactId[] origins = originFeatures.toArray(new ArtifactId[0]);
if (Arrays.equals(origins,result.getFeatureOrigins())) {
return result;
}
else {
result = result.copy(result.getId());
result.setFeatureOrigins(origins);
return result;
}
}
private static boolean match(final ArtifactId id, final ArtifactId override) {
int matchCount = 0;
// check group id
if (BuilderContext.COORDINATE_MATCH_ALL.equals(override.getGroupId())) {
matchCount++;
} else if (id.getGroupId().equals(override.getGroupId())) {
matchCount++;
}
// check artifact id
if (BuilderContext.COORDINATE_MATCH_ALL.equals(override.getArtifactId())) {
matchCount++;
} else if (id.getArtifactId().equals(override.getArtifactId())) {
matchCount++;
}
// check type
if (BuilderContext.COORDINATE_MATCH_ALL.equals(override.getType())) {
matchCount++;
} else if (id.getType().equals(override.getType())) {
matchCount++;
}
// check classifier
if (BuilderContext.COORDINATE_MATCH_ALL.equals(override.getClassifier())) {
matchCount++;
} else if (Objects.equals(id.getClassifier(), override.getClassifier())) {
matchCount++;
}
return matchCount == 4;
}
private static boolean match(final Configuration config, String override) {
boolean result;
if (override.equals("*")) {
result = true;
}
else if (Configuration.isFactoryConfiguration(config.getPid())) {
if (Configuration.isFactoryConfiguration(override)) {
String configPID = Configuration.getFactoryPid(config.getPid());
String overridePID = Configuration.getFactoryPid(override);
if (match(configPID, overridePID)) {
String configName = Configuration.getName(config.getPid());
String overrideName = Configuration.getName(override);
result = match(configName, overrideName);
}
else {
result = false;
}
}
else {
result = false;
}
}
else {
result = match(config.getPid(), override);
}
return result;
}
private static boolean match(String value, String override) {
boolean result;
if (override.endsWith("*")) {
override = override.substring(0, override.length() - 1);
result = value.startsWith(override);
}
else {
result = value.equals(override);
}
return result;
}
private static Set<ArtifactId> getCommonArtifactIds(Artifact a1, Artifact a2) {
final Set<ArtifactId> result = new HashSet<>();
for (final ArtifactId id : a1.getAliases(true)) {
for (final ArtifactId c : a2.getAliases(true)) {
if (id.isSame(c)) {
result.add(id);
}
}
}
return result;
}
private static void findAliasedArtifacts(final ArtifactId id, final Artifacts targetBundles,
final Set<Artifact> result) {
for (final Artifact a : targetBundles) {
for (final ArtifactId aid : a.getAliases(false)) {
if (aid.isSame(id)) {
result.add(a);
}
}
}
}
// configurations - merge / override
static void mergeConfigurations(final Configurations target,
final Configurations source,
final Map<String, String> overrides,
final ArtifactId sourceFeatureId) {
for(final Configuration cfg : source) {
final List<ArtifactId> sourceOrigins = cfg.getFeatureOrigins().isEmpty() ? Collections.singletonList(sourceFeatureId) : cfg.getFeatureOrigins();
Configuration found = target.getConfiguration(cfg.getPid());
if ( found != null ) {
boolean handled = false;
outer:
for (Map.Entry<String, String> override : overrides.entrySet()) {
if (match(cfg, override.getKey())) {
if (BuilderContext.CONFIG_USE_LATEST.equals(override.getValue())) {
final int idx = target.indexOf(found);
target.remove(found);
found = cfg.copy(cfg.getPid());
target.add(idx, found);
setPropertyFeatureOrigins(found, sourceFeatureId);
handled = true;
} else if (BuilderContext.CONFIG_FAIL_ON_PROPERTY_CLASH.equals(override.getValue())){
for (final String key : listProperties(cfg)) {
if (found.getProperties().get(key) != null) {
break outer;
} else {
found.getProperties().put(key, cfg.getProperties().get(key));
found.setFeatureOrigins(key, cfg.getFeatureOrigins(key, sourceFeatureId));
}
}
handled = true;
} else if (BuilderContext.CONFIG_MERGE_LATEST.equals(override.getValue())) {
for (final String key : listProperties(cfg)) {
found.getProperties().put(key, cfg.getProperties().get(key));
final List<ArtifactId> propOrigins = new ArrayList<>(found.getFeatureOrigins(key));
propOrigins.addAll(cfg.getFeatureOrigins(key, sourceFeatureId));
found.setFeatureOrigins(key, propOrigins);
}
handled = true;
} else if (BuilderContext.CONFIG_USE_FIRST.equals(override.getValue())) {
// no need to update property origins
handled = true;
found = null;
} else if (BuilderContext.CONFIG_MERGE_FIRST.equals(override.getValue())) {
for (final String key : listProperties(cfg)) {
if (found.getProperties().get(key) == null) {
found.getProperties().put(key, cfg.getProperties().get(key));
found.setFeatureOrigins(key, cfg.getFeatureOrigins(key, sourceFeatureId));
}
}
handled = true;
}
break outer;
}
}
if (!handled) {
throw new IllegalStateException("Configuration override rule required to select between configurations for " +
cfg.getPid());
}
} else {
// create new configuration
found = cfg.copy(cfg.getPid());
target.add(found);
setPropertyFeatureOrigins(found, sourceFeatureId);
if ( !found.getFeatureOrigins().isEmpty() ) {
found = null;
}
}
if ( found != null ) {
// update origin
final List<ArtifactId> origins = new ArrayList<>(found.getFeatureOrigins());
origins.addAll(sourceOrigins);
found.setFeatureOrigins(origins);
}
}
}
/**
* List all properties but filter all properties for the origin handling
*
* @param cfg The configuration
* @return The filtered list
*/
private static Iterable<String> listProperties(final Configuration cfg) {
return Collections.list(cfg.getProperties().keys())
.stream()
.filter(name -> !name.startsWith(Configuration.PROP_FEATURE_ORIGINS)).collect(Collectors.toList());
}
private static void setPropertyFeatureOrigins(final Configuration cfg, final ArtifactId sourceFeatureId) {
for(final String propName : Collections.list(cfg.getConfigurationProperties().keys())) {
cfg.setFeatureOrigins(propName, cfg.getFeatureOrigins(propName, sourceFeatureId));
}
}
/**
* Merge the framework properties
* @param targetFeature The target feature
* @param sourceFeature The source feature
* @param context Non-null map of overrides
*/
static void mergeFrameworkProperties(final Feature targetFeature, final Feature sourceFeature, final Map<String,String> overrides) {
final Map<String,String> result = new LinkedHashMap<>();
final Map<String, Map<String, Object>> metadata = new HashMap<>();
// keep values from target features, no need to update origin
for (Map.Entry<String, String> entry : targetFeature.getFrameworkProperties().entrySet()) {
result.put(entry.getKey(), overrides.containsKey(entry.getKey()) ? overrides.get(entry.getKey()) : entry.getValue());
metadata.put(entry.getKey(), targetFeature.getFrameworkPropertyMetadata(entry.getKey()));
}
// merge in from source feature
for (Map.Entry<String, String> entry : sourceFeature.getFrameworkProperties().entrySet()) {
final List<ArtifactId> targetIds = (metadata.containsKey(entry.getKey())) ?
new ArrayList<>(targetFeature.getFeatureOrigins(metadata.get(entry.getKey()))) : new ArrayList<>();
targetIds.add(sourceFeature.getId());
if ( overrides.containsKey(entry.getKey()) ) {
result.put(entry.getKey(), overrides.get(entry.getKey()));
} else {
String value = entry.getValue();
if (value != null) {
String targetValue = targetFeature.getFrameworkProperties().get(entry.getKey());
if (targetValue != null) {
if (!value.equals(targetValue)) {
throw new IllegalStateException(String.format("Can't merge framework property '%s' defined twice (as '%s' v.s. '%s') and not overridden.", entry.getKey(), value, targetValue));
}
} else {
result.put(entry.getKey(), value);
}
} else if (!targetFeature.getFrameworkProperties().containsKey(entry.getKey())) {
result.put(entry.getKey(), value);
}
}
metadata.put(entry.getKey(), new HashMap<>(sourceFeature.getFrameworkPropertyMetadata(entry.getKey())));
targetFeature.setFeatureOrigins(metadata.get(entry.getKey()), targetIds);
}
targetFeature.getFrameworkProperties().clear();
targetFeature.getFrameworkProperties().putAll(result);
for(final Map.Entry<String, Map<String, Object>> entry : metadata.entrySet()) {
targetFeature.getFrameworkPropertyMetadata(entry.getKey()).putAll(entry.getValue());
}
}
// requirements (add)
static void mergeRequirements(final List<MatchingRequirement> target, final List<MatchingRequirement> source) {
for (final MatchingRequirement req : source) {
if ( !target.contains(req) ) {
target.add(req);
}
}
}
// capabilities (add)
static void mergeCapabilities(final List<Capability> target, final List<Capability> source) {
for(final Capability cap : source) {
if ( !target.contains(cap) ) {
target.add(cap);
}
}
}
/**
* Merge an extension from source into target
*
* @param target The target extension
* @param source The source extension
* @param sourceFeature Optional, if set origin will be recorded for artifacts
* @param artifactOverrides The merge algorithm for artifacts
* @param originKey
*/
static void mergeExtensions(final Extension target,
final Extension source,
final Feature sourceFeature,
final List<ArtifactId> artifactOverrides,
final String originKey) {
switch ( target.getType() ) {
case TEXT : // simply append
target.setText((target.getText() != null && !target.getText().trim().isEmpty() ? (target.getText() + "\n") : "") + source.getText());
break;
case JSON : JsonStructure struct1;
if (target.getJSON() != null) {
try (final StringReader reader = new StringReader(target.getJSON())) {
struct1 = Json.createReader(reader).read();
}
JsonStructure struct2;
try (final StringReader reader = new StringReader(source.getJSON())) {
struct2 = Json.createReader(reader).read();
}
if (struct1.getValueType() != struct2.getValueType()) {
throw new IllegalStateException("Found different JSON types for extension " + target.getName()
+ " : " + struct1.getValueType() + " and " + struct2.getValueType());
}
if (struct1.getValueType() == ValueType.ARRAY) {
final JsonArrayBuilder builder = Json.createArrayBuilder();
Stream.concat(
((JsonArray) struct1).stream(),
((JsonArray) struct2).stream()
).forEachOrdered(builder::add);
struct1 = builder.build();
} else {
// object is merge
struct1 = merge((JsonObject) struct1, (JsonObject) struct2);
}
StringWriter buffer = new StringWriter();
try (JsonWriter writer = Json.createWriter(buffer)) {
writer.write(struct1);
}
target.setJSON(buffer.toString());
} else {
target.setJSON(source.getJSON());
}
break;
case ARTIFACTS:
mergeArtifacts(target.getArtifacts(), source.getArtifacts(), sourceFeature, artifactOverrides, originKey);
break;
}
}
// extensions (add/merge)
static void mergeExtensions(final Feature target,
final Feature source,
final BuilderContext context,
final List<ArtifactId> artifactOverrides,
final String originKey,
final boolean prototypeMerge,
final boolean initialMerge) {
for(final Extension ext : source.getExtensions()) {
boolean found = false;
// Make a defensive copy of the extensions, as the handlers may modify the extensions on the target
for(final Extension current : new ArrayList<>(target.getExtensions())) {
if ( current.getName().equals(ext.getName()) ) {
found = true;
if ( current.getType() != ext.getType() ) {
throw new IllegalStateException("Found different types for extension " + current.getName()
+ " : " + current.getType() + " and " + ext.getType());
}
boolean handled = false;
for(final MergeHandler me : context.getMergeExtensions()) {
if ( me.canMerge(current) ) {
me.merge(new HandlerContextImpl(context, me, prototypeMerge, initialMerge), target, source, current, ext);
handled = true;
break;
}
}
if ( !handled ) {
// default merge
mergeExtensions(current, ext, source, artifactOverrides, originKey);
}
}
}
if ( !found ) {
// The extension isn't found in the target, still call merge to allow handlers to operate on the
// first extension being aggregated
boolean handled = false;
for (final MergeHandler mh : context.getMergeExtensions()) {
if (mh.canMerge(ext)) {
mh.merge(new HandlerContextImpl(context, mh, prototypeMerge, initialMerge), target, source, null, ext);
handled = true;
break;
}
}
if ( !handled ) {
// no merge handler, just merge with an empty target
Extension current = new Extension(ext.getType(), ext.getName(), ext.getState());
target.getExtensions().add(current);
mergeExtensions(current, ext, source, artifactOverrides, originKey);
}
}
}
// post processing
// Make a defensive copy of the extensions, as the handlers may modify the extensions on the target
for(final Extension ext : new ArrayList<>(target.getExtensions())) {
for(final PostProcessHandler ppe : context.getPostProcessExtensions()) {
ppe.postProcess(new HandlerContextImpl(context, ppe, prototypeMerge, initialMerge), target, ext);
}
}
}
private static JsonObject merge(final JsonObject obj1, final JsonObject obj2) {
JsonObjectBuilder builder = Json.createObjectBuilder();
for (final Map.Entry<String, JsonValue> entry : obj1.entrySet()) {
builder.add(entry.getKey(), entry.getValue());
}
for(final Map.Entry<String, JsonValue> entry : obj2.entrySet()) {
if ( !obj1.containsKey(entry.getKey()) ) {
builder.add(entry.getKey(), entry.getValue());
} else {
final JsonValue oldValue = obj1.get(entry.getKey());
if ( oldValue.getValueType() != entry.getValue().getValueType() ) {
// new type wins
builder.add(entry.getKey(), entry.getValue());
} else if ( oldValue.getValueType() == ValueType.ARRAY ) {
final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
Stream.concat(
((JsonArray) oldValue).stream(),
((JsonArray)entry.getValue()).stream()
).forEachOrdered(arrayBuilder::add);
builder.add(entry.getKey(), arrayBuilder.build());
} else if ( oldValue.getValueType() == ValueType.OBJECT ) {
builder.add(entry.getKey(), merge((JsonObject)oldValue, (JsonObject)entry.getValue()));
} else {
builder.add(entry.getKey(), entry.getValue());
}
}
}
return builder.build();
}
static class HandlerContextImpl implements HandlerContext {
private final ArtifactProvider artifactProvider;
private final Map<String,String> configuration;
private final boolean prototypeMerge;
private final boolean initialMerge;
HandlerContextImpl(BuilderContext bc, MergeHandler handler, final boolean prototype, final boolean initial) {
artifactProvider = bc.getArtifactProvider();
configuration = getHandlerConfiguration(bc, handler);
this.prototypeMerge = prototype;
this.initialMerge = initial;
}
HandlerContextImpl(BuilderContext bc, PostProcessHandler handler, final boolean prototype, final boolean initial) {
artifactProvider = bc.getArtifactProvider();
configuration = getHandlerConfiguration(bc, handler);
this.prototypeMerge = prototype;
this.initialMerge = initial;
}
private Map<String,String> getHandlerConfiguration(BuilderContext bc, Object handler) {
final Map<String,String> result = new HashMap<>();
Map<String, String> overall = bc.getHandlerConfigurations()
.get(BuilderContext.CONFIGURATION_ALL_HANDLERS_KEY);
if (overall != null)
result.putAll(overall);
final String name = getHandlerName(handler);
if (name != null) {
Map<String, String> handlerSpecific = bc.getHandlerConfigurations().get(name);
if (handlerSpecific != null)
result.putAll(handlerSpecific);
}
return result;
}
private static String getHandlerName(Object handler) {
return handler.getClass().getSimpleName();
}
@Override
public ArtifactProvider getArtifactProvider() {
return artifactProvider;
}
@Override
public Map<String,String> getConfiguration() {
return configuration;
}
@Override
public boolean isInitialMerge() {
return this.initialMerge;
}
@Override
public boolean isPrototypeMerge() {
return this.prototypeMerge;
}
}
}