/** * Copyright (c) 2009 - 2012 Red Hat, Inc. * * This software is licensed to you under the GNU General Public License, * version 2 (GPLv2). There is NO WARRANTY for this software, express or * implied, including the implied warranties of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 * along with this software; if not, see * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. * * Red Hat trademarks are not licensed under GPLv2. No permission is * granted to use or replicate Red Hat trademarks that are incorporated * in this software or its documentation. */ package org.candlepin.sync; import org.candlepin.model.Owner; import org.candlepin.model.Pool; import org.candlepin.model.Pool.PoolType; import org.candlepin.model.PoolCurator; import org.candlepin.model.dto.Subscription; import com.google.inject.Inject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * Reconciles incoming subscriptions from an import against pre-existing pools in the * database. * * Comparisons are made and when we detect an incoming subscription that looks * similar to an existing pool, but from a new upstream entitlement, we avoid destroying * the pool and mass cert revocation and treat them as if they are the same * subscription. * */ public class SubscriptionReconciler { private static Logger log = LoggerFactory.getLogger(SubscriptionReconciler.class); private PoolCurator poolCurator; @Inject public SubscriptionReconciler(PoolCurator poolCurator) { this.poolCurator = poolCurator; } /** * Reconciles incoming entitlements to existing pools to attempt to limit * the number of pools we must destroy and revoke. * * Each set is mapped against the upstream pool ID. Subscriptions originating from * different pools will never match each other for replacement. * * First match attempts a direct match of entitlement ID from incoming * entitlements for comparison to existing pools. If so we re-use the existing pool. * (note that despite the same entitlement ID, quantity and other data may have * changed) * * Next we attempt to match on exact quantity. This would allow the user to re-create * a distributor upstream and re-assign it the same entitlement quantities from the * same pools, without triggering a mass regen on import. * * The last attempt will order the remaining incoming entitlements by quantity, and * match these against a quantity ordered list of the remaining pools for that * upstream pool. * * Remaining incoming subscriptions that did not match any pools per the above are * treated as new subscriptions. * * @param owner * The owner for which the subscriptions are being imported * * @param subsToImport * A collection of subscriptions which are being imported * * @return * The collection of reconciled subscriptions */ public Collection<Subscription> reconcile(Owner owner, Collection<Subscription> subsToImport) { Map<String, Map<String, Pool>> existingPoolsByUpstreamPool = this.mapPoolsByUpstreamPool(owner); // if we can match to the entitlement id do it. // we need a new list to hold the ones that are left Set<Subscription> subscriptionsStillToImport = new HashSet<Subscription>(); for (Subscription subscription : subsToImport) { Pool local = null; Map<String, Pool> map = existingPoolsByUpstreamPool.get(subscription.getUpstreamPoolId()); if (map == null || map.isEmpty()) { log.info("New subscription for incoming entitlement ID: {}", subscription.getUpstreamEntitlementId()); continue; } local = map.get(subscription.getUpstreamEntitlementId()); if (local != null) { mergeSubscription(subscription, local, map); log.info("Merging subscription for incoming entitlement id [{}] into subscription with " + "existing entitlement id [{}]. Entitlement id match.", subscription.getUpstreamEntitlementId(), local.getUpstreamEntitlementId() ); } else { subscriptionsStillToImport.add(subscription); log.warn("Subscription for incoming entitlement id [{}] does not have an entitlement id " + "match in the current subscriptions for the upstream pool id [{}]", subscription.getUpstreamEntitlementId(), subscription.getUpstreamPoolId() ); } } // matches will be made against the upstream pool id and quantity. // we need a new list to hold the ones that are left List<Subscription> subscriptionsNeedQuantityMatch = new ArrayList<Subscription>(); for (Subscription subscription : subscriptionsStillToImport) { Pool local = null; Map<String, Pool> map = existingPoolsByUpstreamPool.get(subscription.getUpstreamPoolId()); if (map == null) { map = new HashMap<String, Pool>(); } for (Pool localSub : map.values()) { // TODO quantity Long quantity = localSub.getQuantity() / localSub.getProduct().getMultiplier(); if (quantity.equals(subscription.getQuantity())) { local = localSub; break; } } if (local != null) { mergeSubscription(subscription, local, map); log.info("Merging subscription for incoming entitlement id [{}] into subscription with " + "existing entitlement id [{}]. Exact quantity match.", subscription.getUpstreamEntitlementId(), local.getUpstreamEntitlementId() ); } else { subscriptionsNeedQuantityMatch.add(subscription); log.warn("Subscription for incoming entitlement id [{}] does not have an exact quantity " + "match in the current subscriptions for the upstream pool id [{}]", subscription.getUpstreamEntitlementId(), subscription.getUpstreamPoolId() ); } } // matches will be made against the upstream pool id and quantity. // quantities will just match by position from highest to lowest // we need a new list to hold the ones that are left Subscription[] inNeed = subscriptionsNeedQuantityMatch.toArray(new Subscription[0]); Arrays.sort(inNeed, new SubQuantityComparator()); for (Subscription subscription : inNeed) { Pool local = null; Map<String, Pool> map = existingPoolsByUpstreamPool.get(subscription.getUpstreamPoolId()); if (map == null || map.isEmpty()) { log.info("Creating new subscription for incoming entitlement with id [{}]", subscription.getUpstreamEntitlementId()); continue; } Pool[] locals = map.values().toArray(new Pool[0]); Arrays.sort(locals, new QuantityComparator()); local = locals[0]; mergeSubscription(subscription, local, map); log.info("Merging subscription for incoming entitlement id [{}] into subscription with " + "existing entitlement id [{}] Ordered quantity match.", subscription.getUpstreamEntitlementId(), local.getUpstreamEntitlementId() ); } return subsToImport; } /* * Maps upstream pool ID to a map of upstream entitlement ID to Subscription. */ private Map<String, Map<String, Pool>> mapPoolsByUpstreamPool(Owner owner) { Map<String, Map<String, Pool>> existingSubsByUpstreamPool = new HashMap<String, Map<String, Pool>>(); int idx = 0; for (Pool p : this.poolCurator.listByOwnerAndType(owner, PoolType.NORMAL)) { // if the upstream pool id is null, // this must be a locally controlled sub. if (p.getUpstreamPoolId() == null) { continue; } /* * If the existing sub does not have the entitlement id yet, * just assign a placeholder to differentiate. * * Suspect this is an old migration path, likely all pools have their * entitlement ID stamped by now. - dgoodwin 2015-04-14 */ if (p.getUpstreamEntitlementId() == null || p.getUpstreamEntitlementId().trim().equals("")) { p.setUpstreamEntitlementId("" + idx++); } Map<String, Pool> subs = existingSubsByUpstreamPool.get(p.getUpstreamPoolId()); if (subs == null) { subs = new HashMap<String, Pool>(); existingSubsByUpstreamPool.put(p.getUpstreamPoolId(), subs); } subs.put(p.getUpstreamEntitlementId(), p); } return existingSubsByUpstreamPool; } private void mergeSubscription(Subscription subscription, Pool local, Map<String, Pool> map) { subscription.setId(local.getSubscriptionId()); map.remove(local.getUpstreamEntitlementId()); } /** * QuantityComparator * * descending quantity sort on Subscriptions */ public static class QuantityComparator implements Comparator<Pool>, Serializable { private static final long serialVersionUID = -5694014081615252430L; @Override public int compare(Pool s1, Pool s2) { return s2.getQuantity().compareTo(s1.getQuantity()); } } /** * SubQuantityComparator * */ public static class SubQuantityComparator implements Comparator<Subscription>, Serializable { private static final long serialVersionUID = -7739774339143293267L; @Override public int compare(Subscription s1, Subscription s2) { return s2.getQuantity().compareTo(s1.getQuantity()); } } }