| /* |
| * 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()); |
| } |
| } |
| } |