/** * 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.liquibase; import liquibase.database.jvm.JdbcConnection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.Statement; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** * FixDuplicatePools class to fix duplicate pool data. * * This class is disconnected from its liquibase wrapper * so that it may be used more easily as a one-off script * in scenarios that do not use liquibase. */ public class FixDuplicatePools { private JdbcConnection conn; private final CustomTaskLogger log; public FixDuplicatePools(JdbcConnection conn) { this.conn = conn; this.log = new SystemOutLogger(); } public FixDuplicatePools(JdbcConnection conn, CustomTaskLogger log) { this.conn = conn; this.log = log; } /* * When we delete pools, we need to reassign entitlements and mark them dirty. * Otherwise they will contain a bad pool id. * * ActivationKeyPool will be deleted by the cascade * source entitlement shouldn't exist for anything with nonnull subid and subkey * * Make sure that Branding is also deleted on cascade on the database level, * hibernate annotations don't apply * */ public void execute() throws Exception { log.info("--- Running update script ---"); // Get a map of source subscription to pool ids Map<SubPair, List<String>> subPoolsMap = getSubPoolMap(); log.info("Found " + subPoolsMap.keySet().size() + " subscriptions " + "with duplicate pools."); // Nothing to do if there aren't any duplicates if (!subPoolsMap.isEmpty()) { for (Entry<SubPair, List<String>> entry : subPoolsMap.entrySet()) { SubPair sub = entry.getKey(); List<String> ids = entry.getValue(); List<String> poolsToRemove = new LinkedList<String>(); // Keep one pool String poolToKeep = ids.get(0); // Remove the rest for (int i = 1, len = ids.size(); i < len; i++) { poolsToRemove.add(ids.get(i)); } log.info("Removing " + poolsToRemove.size() + " pools for subscription " + sub); updateEntsForIds(poolsToRemove, poolToKeep); removePoolsWithIds(poolsToRemove); } } log.info("--- Finished update script ---"); } private int removePoolsWithIds(List<String> dupeIds) throws Exception { String sql = "DELETE FROM cp_pool " + "WHERE id IN ("; for (int i = 0, len = dupeIds.size(); i < len; i++) { sql += "?"; if (i < len - 1) { sql += ", "; } } sql += ")"; PreparedStatement stmt = conn.prepareStatement(sql); for (int i = 0, max = dupeIds.size(); i < max; i++) { stmt.setString(i + 1, dupeIds.get(i)); } return stmt.executeUpdate(); } private int updateEntsForIds(List<String> dupeIds, String goodId) throws Exception { String sql = "UPDATE cp_entitlement " + "SET pool_id=?, dirty=? " + "WHERE pool_id IN ("; for (int i = 0, len = dupeIds.size(); i < len; i++) { sql += "?"; if (i != len - 1) { sql += ", "; } } sql += ")"; PreparedStatement stmt = conn.prepareStatement(sql); stmt.setString(1, goodId); // Different databases represent booleans differently. Even though this // is a static value, we need to let the jdbc driver handle it stmt.setBoolean(2, true); for (int i = 0, max = dupeIds.size(); i < max; i++) { stmt.setString(i + 3, dupeIds.get(i)); } return stmt.executeUpdate(); } /* * Builds a map of subid-subkey -> [ids, with, duplicates] */ private Map<SubPair, List<String>> getSubPoolMap() throws Exception { Statement stmt = conn.createStatement(); Map<SubPair, List<String>> subPoolsMap = new HashMap<SubPair, List<String>>(); ResultSet rs = stmt.executeQuery( "SELECT cp_pool.id, cp_pool.subscriptionid, " + "cp_pool.subscriptionsubkey, cp_pool.owner_id " + "FROM cp_pool, " + "(SELECT subscriptionid, subscriptionsubkey " + "FROM cp_pool " + "WHERE subscriptionid IS NOT NULL " + "AND subscriptionsubkey IS NOT NULL " + "GROUP BY subscriptionid, subscriptionsubkey " + "HAVING count(id) > 1) subs " + "WHERE subs.subscriptionid = cp_pool.subscriptionid " + "AND subs.subscriptionsubkey = cp_pool.subscriptionsubkey"); // We need to be completely sure that we aren't moving subscriptions // from one owner to another. Map<SubPair, String> subOwners = new HashMap<SubPair, String>(); while (rs.next()) { SubPair current = new SubPair(rs.getString(2), rs.getString(3)); if (!subPoolsMap.containsKey(current)) { subPoolsMap.put(current, new LinkedList<String>()); subOwners.put(current, rs.getString(4)); } else if (!subOwners.get(current).equals(rs.getString(4))) { // Make sure owners are the same log.error("Owners '" + rs.getString(4) + "' and '" + subOwners.get(current) + "' both have pools from subscription " + current); throw new Exception("Pools exist for subscription " + current + " within " + "multiple owners."); } subPoolsMap.get(current).add(rs.getString(1)); } return subPoolsMap; } /** * Conainer for subscriptionid/subscriptionsubkey values * * I suppose concatenating the strings in our sql query would do * this just as well... */ public class SubPair { private String subscriptionId; private String subscriptionSubKey; public SubPair(String id, String key) { this.subscriptionId = id; this.subscriptionSubKey = key; } @Override public boolean equals(Object o) { if (o != null && o instanceof SubPair) { SubPair other = (SubPair) o; return this.subscriptionId.equals(other.subscriptionId) && this.subscriptionSubKey.equals(other.subscriptionSubKey); } return false; } @Override public int hashCode() { return (subscriptionId + subscriptionSubKey).hashCode(); } @Override public String toString() { return this.subscriptionId + "-" + this.subscriptionSubKey; } } }