blob: 61b2f9cebaf7c26fca2572d5fa84c7700423013d [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.discovery.commons;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.apache.sling.discovery.ClusterView;
import org.apache.sling.discovery.InstanceDescription;
import org.apache.sling.discovery.InstanceFilter;
import org.apache.sling.discovery.TopologyEvent;
import org.apache.sling.discovery.TopologyView;
/**
* The {@code InstancesDiff} allows to combine and filter two collections of {@code InstanceDescription} instances,
* an "old" collection and a "new" collection.<p>
*
* The comparison between {@code InstanceDescription} instances is done only on the basis of the Sling identifier.
* Two instances with the same Sling identifier are considered as equal.<p>
*
* <b>Note</b>: Each collection must contain only unique instances (no two instances with the same Sling identifier).
* Using the {@code InstancesDiff} with collections containing duplicated Sling id
* will throw an {@code IllegalArgumentException}.<p>
*
* @since 1.0.0
*/
public final class InstancesDiff {
/**
* A filter that keeps local instances (see {@link InstanceDescription#isLocal()}.
*/
private static final InstanceFilter LOCAL_INSTANCE = new LocalInstanceFilter();
/**
* A filter that filters out local instances (see {@link InstanceDescription#isLocal()}.
*/
private static final InstanceFilter NOT_LOCAL_INSTANCE = new NotFilter(LOCAL_INSTANCE);
/**
* A filter that keeps leader instances (see {@link InstanceDescription#isLeader()}.
*/
private static final InstanceFilter LEADER_INSTANCE = new LeaderInstanceFilter();
/**
* A filter that filters out leader instances (see {@link InstanceDescription#isLeader()}.
*/
private static final InstanceFilter NOT_LEADER_INSTANCE = new NotFilter(LEADER_INSTANCE);
/**
* Keeps track of the old {@code InstanceDescription} instances
*
* The map keys are the instance Sling identifiers and values are
* the {@code InstanceDescription} instances descriptions.
*/
private final Map<String, InstanceDescription> oldInstances;
/**
* Keeps track of the new {@code InstanceDescription} instances
*
* The map keys are the instance Sling identifiers and values are
* the {@code InstanceDescription} instances descriptions.
*/
private final Map<String, InstanceDescription> newInstances;
/**
* Create a new {@code InstancesDiff} based on the instances from the old and
* new {@code TopologyView} topology views contained in the {@code TopologyEvent} event provided.
*
* @param event the non {@code null} event from which the old and new topology views are used for computing.
* If either of the topology views are {@code null}, then they will be substituted by an
* empty collection of instances.
* @throws IllegalArgumentException if either of the collections contains duplicated Sling identifiers.
*/
public InstancesDiff(@Nonnull TopologyEvent event) {
this(instancesOrEmpty(event.getOldView()), instancesOrEmpty(event.getNewView()));
}
/**
* Create a new {@code InstancesDiff} based on the instances from the old and
* new {@code TopologyView} topology views provided.
*
* @param oldView the non {@code null} old topology view from which the old collection is used for computing.
* @param newView the non {@code null} new topology view form which the new collection is used for computing.
* @throws IllegalArgumentException if either of the collections contains duplicated Sling identifiers.
*/
public InstancesDiff(@Nonnull TopologyView oldView, @Nonnull TopologyView newView) {
this(oldView.getInstances(), newView.getInstances());
}
/**
* Create a new {@code InstancesDiff} based on the instances from the old and
* new {@code ClusterView} cluster views provided.
*
* @param oldView the non {@code null} old cluster view used for computing.
* @param newView the non {@code null} new cluster view used for computing.
* @throws IllegalArgumentException if either of the collections contains duplicated Sling identifiers.
*/
public InstancesDiff(@Nonnull ClusterView oldView, @Nonnull ClusterView newView) {
this(oldView.getInstances(), newView.getInstances());
}
/**
* Create a new {@code InstancesDiff} based on the provided old and
* new {@code Collection} collections of instances.
*
* @param oldInstances the non {@code null} old collection of instances used for computing.
* @param newInstances the non {@code null} new collection of instances used for computing.
* @param <T> the type of instance which must extend {@code InstanceDescription}.
* @throws IllegalArgumentException if either of the collections contains duplicated Sling identifiers.
*/
public <T extends InstanceDescription> InstancesDiff(@Nonnull Collection<T> oldInstances, @Nonnull Collection<T> newInstances) {
this.newInstances = getInstancesMap(newInstances);
this.oldInstances = getInstancesMap(oldInstances);
}
/**
* Returns the {@code InstanceSet} set containing the {@code InstanceDescription} instances that are
* contained in either the old or the new collection.<p>
*
* For {@code InstanceDescription} instances contained in both the old and
* the new collections, the method will retain those from either of the collections
* depending on the parameter #retainFromNewView.<p>
*
* @param retainFromNewCollection {@code true} in order to retain the instances from the new collection ;
* {@code false} in order to retain the instances from the old collection.
* @return the {@code InstanceCollection} collection containing the {@code InstanceDescription} instances
* from both collections.
*/
@Nonnull
public InstanceCollection all(boolean retainFromNewCollection) {
return new InstanceCollection(partitionAll(retainFromNewCollection));
}
/**
* Returns the {@code InstanceCollection} collection containing the {@code InstanceDescription} instances that are
* contained in the new collection but not in the old collection.
*
* @return the {@code InstanceCollection} collection containing the instances in the new
* topology collection but not in the old collection.
*/
@Nonnull
public InstanceCollection added() {
return new InstanceCollection(partitionAdded());
}
/**
* Returns the {@code InstanceCollection} collection containing the {@code InstanceDescription} instances that are
* contained in the old collection but not in the new collection.
*
* @return the {@code InstanceSet} set containing the instances in the old collection but not in the new collection.
*/
@Nonnull
public InstanceCollection removed() {
return new InstanceCollection(partitionRemoved());
}
/**
* Returns the {@code InstanceSet} collection containing the {@code InstanceDescription} instances that are
* contained in both the old collection and the new collection.<p>
*
* The method will retain the {@code InstanceDescription} instances from either of the collections
* depending on the parameter #retainFromNewView.<p>
*
* @param retainFromNewCollection {@code true} in order to retain the instances from the new collection ;
* {@code false} in order to retain the instances from the old collection.
* @return the {@code InstanceCollection} collection containing the {@code InstanceDescription} instances
* contained in both collections.
*/
@Nonnull
public InstanceCollection retained(boolean retainFromNewCollection) {
return new InstanceCollection(partitionRetained(retainFromNewCollection));
}
/**
* Returns the {@code InstanceCollection} collection containing the {@code InstanceDescription} instances that are
* contained in both the old and the new collections.<p>
*
* The method will retain the {@code InstanceDescription} instances from either of the collections
* depending on the parameter #retainFromNewView.<p>
*
* @param retainFromNewCollection {@code true} in order to retain the instances from the new collection ;
* {@code false} in order to retain the instances from the old collection.
* @param propertyChanged {@code true} in order to keep only the instances which
* properties have not changed between the old and new collections ;
* {@code false} in order to keep only the instances which properties have changed.
* @return the {@code InstanceCollection} collection containing the {@code InstanceDescription} instances
* contained in both views.
*/
@Nonnull
public InstanceCollection retained(boolean retainFromNewCollection, boolean propertyChanged) {
return new InstanceCollection(partitionRetained(retainFromNewCollection, propertyChanged));
}
//
@Nonnull
private Map<String, InstanceDescription> partitionAll(boolean retainFromNewCollection) {
Map<String, InstanceDescription> partition = new HashMap<String, InstanceDescription>();
if (retainFromNewCollection) {
partition.putAll(oldInstances);
partition.putAll(newInstances);
} else {
partition.putAll(newInstances);
partition.putAll(oldInstances);
}
return partition;
}
@Nonnull
private Map<String, InstanceDescription> partitionRemoved() {
Map<String, InstanceDescription> partition = new HashMap<String, InstanceDescription>(oldInstances);
partition.keySet().removeAll(newInstances.keySet());
return partition;
}
@Nonnull
private Map<String, InstanceDescription> partitionAdded() {
Map<String, InstanceDescription> partition = new HashMap<String, InstanceDescription>(newInstances);
partition.keySet().removeAll(oldInstances.keySet());
return partition;
}
@Nonnull
private Map<String, InstanceDescription> partitionRetained(boolean retainFromNewCollection, boolean propertyChanged) {
Map<String, InstanceDescription> partition = new HashMap<String, InstanceDescription>();
for (Map.Entry<String, InstanceDescription> oldEntry : oldInstances.entrySet()) {
String slingId = oldEntry.getKey();
InstanceDescription newDescription = newInstances.get(slingId);
if(newDescription != null) {
InstanceDescription oldDescription = oldEntry.getValue();
boolean propertiesSame = newDescription.getProperties().equals(oldDescription.getProperties());
if ((propertiesSame && ! propertyChanged) || (! propertiesSame && propertyChanged)) {
partition.put(slingId, retainFromNewCollection ? newDescription : oldDescription);
}
}
}
return partition;
}
@Nonnull
private Map<String, InstanceDescription> partitionRetained(boolean retainFromNewCollection) {
Map<String, InstanceDescription> partition = new HashMap<String, InstanceDescription>();
if (retainFromNewCollection) {
partition.putAll(newInstances);
partition.keySet().retainAll(oldInstances.keySet());
} else {
partition.putAll(oldInstances);
partition.keySet().retainAll(newInstances.keySet());
}
return partition;
}
@Nonnull
private static Set<InstanceDescription> instancesOrEmpty(@Nullable TopologyView topologyView) {
return (topologyView != null) ? topologyView.getInstances() : Collections.<InstanceDescription>emptySet();
}
@Nonnull
private static <T extends InstanceDescription> Map<String, InstanceDescription> getInstancesMap(@Nonnull Collection<T> instances) {
Map<String, InstanceDescription> instancesMap = new HashMap<String, InstanceDescription>();
for (InstanceDescription instance : instances) {
String slingId = instance.getSlingId();
if (slingId != null) {
if (instancesMap.put(slingId, instance) != null) {
throw new IllegalArgumentException(String.format("Duplicated instance found for slingId: %s", slingId));
}
}
}
return instancesMap;
}
private static final class NotFilter implements InstanceFilter {
final InstanceFilter filter;
private NotFilter(InstanceFilter filter) {
this.filter = filter;
}
public boolean accept(InstanceDescription instance) {
return ! filter.accept(instance);
}
}
private static final class LocalInstanceFilter implements InstanceFilter {
public boolean accept(InstanceDescription instance) {
return instance.isLocal();
}
}
private static final class LeaderInstanceFilter implements InstanceFilter {
public boolean accept(InstanceDescription instance) {
return instance.isLeader();
}
}
private static final class InClusterView implements InstanceFilter {
private final ClusterView view;
private InClusterView(ClusterView view) {
this.view = view;
}
public boolean accept(InstanceDescription instance) {
return view.getId().equals(instance.getClusterView().getId());
}
}
/**
* The {@code InstanceCollection} collection allows to filter the instances using a set of custom filter
* either implementing {@code InstanceFilter} or pre-defined ones.<p>
*
* Filters conditions are joined combined together using the logical operator "AND".<p>
*/
public final class InstanceCollection {
/**
* Holds the instances to be filtered.
*
* The map keys are the instance Sling identifiers and values are
* the {@code InstanceDescription} instances descriptions.
*/
private final Map<String, InstanceDescription> instances;
/**
* Holds the set of filters to be applied (ANDed).
*/
private final Set<InstanceFilter> filters = new HashSet<InstanceFilter>();
/**
* Filter the instances with a custom {@code InstanceFilter} filter.
*
* @param filter the filter to be applied on the instances
* @return {@code this}
*/
@Nonnull
public InstanceCollection filterWith(@Nullable InstanceFilter filter) {
if (filter != null) {
filters.add(filter);
}
return this;
}
/**
* Keep only the local instance (see {@link InstanceDescription#isLocal()}.
*
* @return {@code this}
*/
@Nonnull
public InstanceCollection isLocal() {
filters.add(LOCAL_INSTANCE);
return this;
}
/**
* Filter out the local instances (see {@link InstanceDescription#isLocal()}.
*
* @return {@code this}
*/
@Nonnull
public InstanceCollection isNotLocal() {
filters.add(NOT_LOCAL_INSTANCE);
return this;
}
/**
* Keep only the leader instances (see {@link InstanceDescription#isLeader()}.
*
* @return {@code this}
*/
@Nonnull
public InstanceCollection isLeader() {
filters.add(LEADER_INSTANCE);
return this;
}
/**
* Filter out the leader instances (see {@link InstanceDescription#isLeader()}.
*
* @return {@code this}
*/
@Nonnull
public InstanceCollection isNotLeader() {
filters.add(NOT_LEADER_INSTANCE);
return this;
}
/**
* Keep only the instances that are contained in the same {@code ClusterView} cluster view
* as the one provided.<p>
*
* The comparison between cluster views is done on the basis of the cluster
* view identifier. Two cluster views with the same identifier are considered equal.<p>
*
* @param clusterView the cluster view used to filter the instances
* @return {@code this}
*/
@Nonnull
public InstanceCollection isInClusterView(@Nullable ClusterView clusterView) {
if (clusterView != null) {
filters.add(new InClusterView(clusterView));
}
return this;
}
/**
* Filter out the instances that are contained in the same {@code ClusterView} cluster view
* as the one provided.<p>
*
* The comparison between cluster views is done on the basis of the cluster
* view identifier. Two cluster views with the same identifier are considered equal.<p>
*
* @param clusterView the cluster view used to filter the instances
* @return {@code this}
*/
@Nonnull
public InstanceCollection isNotInClusterView(@Nullable ClusterView clusterView) {
if (clusterView != null) {
filters.add(new NotFilter(new InClusterView(clusterView)));
}
return this;
}
/**
* Return the collection of {@code InstanceDescription} instances that have not been filtered out.
*
* @return the filtered collection of instances.
*/
@Nonnull
public Collection<InstanceDescription> get() {
return applyFilters();
}
//
/**
* Instances of this class can only be obtained through the {@code InstancesDiff} class.
* @param instances the map of instances to be filtered
*/
private InstanceCollection(@Nonnull Map<String, InstanceDescription> instances) {
this.instances = instances;
}
@Nonnull
private Collection<InstanceDescription> applyFilters() {
Iterator<Map.Entry<String, InstanceDescription>> entries = instances.entrySet().iterator();
for ( ; entries.hasNext() ; ) {
Map.Entry<String, InstanceDescription> entry = entries.next();
for (InstanceFilter filter : filters) {
if (! filter.accept(entry.getValue())) {
entries.remove();
break;
}
}
}
return Collections.<InstanceDescription>unmodifiableCollection(instances.values());
}
}
}