/** * 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.model; import org.candlepin.model.activationkeys.ActivationKey; import com.google.inject.persist.Transactional; import org.hibernate.Criteria; import org.hibernate.Session; import org.hibernate.criterion.DetachedCriteria; import org.hibernate.criterion.Disjunction; import org.hibernate.criterion.Order; import org.hibernate.criterion.Projections; import org.hibernate.criterion.Restrictions; import org.hibernate.sql.JoinType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * The OwnerProductCurator provides functionality for managing the mapping between owners and * products. */ public class OwnerProductCurator extends AbstractHibernateCurator<OwnerProduct> { private static Logger log = LoggerFactory.getLogger(OwnerProductCurator.class); /** * Default constructor */ public OwnerProductCurator() { super(OwnerProduct.class); } public Product getProductById(Owner owner, String productId) { return this.getProductById(owner.getId(), productId); } @Transactional public Product getProductById(String ownerId, String productId) { return (Product) this.createSecureCriteria() .createAlias("owner", "owner") .createAlias("product", "product") .setProjection(Projections.property("product")) .add(Restrictions.eq("owner.id", ownerId)) .add(Restrictions.eq("product.id", productId)) .uniqueResult(); } public CandlepinQuery<Owner> getOwnersByProduct(Product product) { return this.getOwnersByProduct(product.getId()); } public CandlepinQuery<Owner> getOwnersByProduct(String productId) { // Impl note: // We have to do this in two queries due to how Hibernate processes projections here. We're // working around a number of issues: // 1. Hibernate does not rearrange a query based on a projection, but instead, performs a // second query (as we're doing here). // 2. Because the initial query is not rearranged, we are actually pulling a collection of // join objects, so filtering/sorting via CandlepinQuery is incorrect or broken // 3. The second query Hibernate performs uses the IN operator without any protection for // the MySQL/MariaDB or Oracle element limits. String jpql = "SELECT op.owner.id FROM OwnerProduct op WHERE op.product.id = :product_id"; List<String> ids = this.getEntityManager() .createQuery(jpql, String.class) .setParameter("product_id", productId) .getResultList(); if (ids != null && !ids.isEmpty()) { DetachedCriteria criteria = this.createSecureDetachedCriteria(Owner.class, null) .add(CPRestrictions.in("id", ids)); return this.cpQueryFactory.<Owner>buildQuery(this.currentSession(), criteria); } return this.cpQueryFactory.<Owner>buildQuery(); } /** * Fetches a collection of product UUIDs currently mapped to the given owner. If the owner is * not mapped to any products, an empty collection will be returned. * * @param owner * The owner for which to fetch product UUIDs * * @return * a collection of product UUIDs belonging to the given owner */ public Collection<String> getProductUuidsByOwner(Owner owner) { return this.getProductUuidsByOwner(owner.getId()); } /** * Fetches a collection of product UUIDs currently mapped to the given owner. If the owner is * not mapped to any products, an empty collection will be returned. * * @param ownerId * The ID of the owner for which to fetch product UUIDs * * @return * a collection of product UUIDs belonging to the given owner */ public Collection<String> getProductUuidsByOwner(String ownerId) { String jpql = "SELECT op.product.uuid FROM OwnerProduct op WHERE op.owner.id = :owner_id"; List<String> uuids = this.getEntityManager() .createQuery(jpql, String.class) .setParameter("owner_id", ownerId) .getResultList(); return uuids != null ? uuids : Collections.<String>emptyList(); } /** * Builds a query for fetching the products currently mapped to the given owner. * * @param owner * The owner for which to fetch products * * @return * a query for fetching the products belonging to the given owner */ public CandlepinQuery<Product> getProductsByOwner(Owner owner) { return this.getProductsByOwner(owner.getId()); } /** * Builds a query for fetching the products currently mapped to the given owner. * * @param ownerId * The ID of the owner for which to fetch products * * @return * a query for fetching the products belonging to the given owner */ public CandlepinQuery<Product> getProductsByOwner(String ownerId) { // Impl note: See getOwnersByProduct for details on why we're doing this in two queries Collection<String> uuids = this.getProductUuidsByOwner(ownerId); if (!uuids.isEmpty()) { DetachedCriteria criteria = this.createSecureDetachedCriteria(Product.class, null) .add(CPRestrictions.in("uuid", uuids)); return this.cpQueryFactory.<Product>buildQuery(this.currentSession(), criteria); } return this.cpQueryFactory.<Product>buildQuery(); } public CandlepinQuery<Product> getProductsByIds(Owner owner, Collection<String> productIds) { return this.getProductsByIds(owner.getId(), productIds); } public CandlepinQuery<Product> getProductsByIds(String ownerId, Collection<String> productIds) { if (productIds == null || productIds.isEmpty()) { return this.cpQueryFactory.<Product>buildQuery(); } // Impl note: See getOwnersByProduct for details on why we're doing this in two queries. String jpql = "SELECT op.product.uuid FROM OwnerProduct op WHERE op.owner.id = :owner_id"; List<String> uuids = this.getEntityManager() .createQuery(jpql, String.class) .setParameter("owner_id", ownerId) .getResultList(); if (uuids != null && !uuids.isEmpty()) { DetachedCriteria criteria = this.createSecureDetachedCriteria(Product.class, null) .add(CPRestrictions.in("uuid", uuids)) .add(CPRestrictions.in("id", productIds)); return this.cpQueryFactory.<Product>buildQuery(this.currentSession(), criteria); } return this.cpQueryFactory.<Product>buildQuery(); } @Transactional public long getOwnerCount(Product product) { String jpql = "SELECT count(op) FROM OwnerProduct op WHERE op.product.uuid = :product_uuid"; long count = (Long) this.getEntityManager() .createQuery(jpql, Long.class) .setParameter("product_uuid", product.getUuid()) .getSingleResult(); return count; } /** * Checks if the owner has an existing version of the specified product. This lookup is * different than the mapping check in that this check will find any product with the * specified ID, as opposed to checking if a specific version is mapped to the owner. * * @param owner * The owner of the product to lookup * * @param productId * The Red Hat ID of the product to lookup * * @return * true if the owner has a product with the given RHID; false otherwise */ @Transactional public boolean productExists(Owner owner, String productId) { String jpql = "SELECT count(op) FROM OwnerProduct op " + "WHERE op.owner.id = :owner_id AND op.product.id = :product_id"; long count = (Long) this.getEntityManager() .createQuery(jpql) .setParameter("owner_id", owner.getId()) .setParameter("product_id", productId) .getSingleResult(); return count > 0; } /** * Filters the given list of Red Hat product IDs by removing the IDs which represent unknown * products for the specified owner. * * @param owner * The owner to search * * @param productIds * A collection of Red Hat product IDs to filter * * @return * A new set containing only product IDs for products which exist for the given owner */ @Transactional public Set<String> filterUnknownProductIds(Owner owner, Collection<String> productIds) { Set<String> existingIds = new HashSet<String>(); if (productIds != null && !productIds.isEmpty()) { existingIds.addAll(this.createSecureCriteria() .createAlias("owner", "owner") .createAlias("product", "product") .setProjection(Projections.property("product.id")) .add(Restrictions.eq("owner.id", owner.getId())) .add(CPRestrictions.in("product.id", productIds)) .list()); } return existingIds; } @Transactional public boolean isProductMappedToOwner(Product product, Owner owner) { String jpql = "SELECT count(op) FROM OwnerProduct op " + "WHERE op.owner.id = :owner_id AND op.product.uuid = :product_uuid"; long count = (Long) this.getEntityManager() .createQuery(jpql) .setParameter("owner_id", owner.getId()) .setParameter("product_uuid", product.getUuid()) .getSingleResult(); return count > 0; } @Transactional public boolean mapProductToOwner(Product product, Owner owner) { if (!this.isProductMappedToOwner(product, owner)) { this.create(new OwnerProduct(owner, product)); return true; } return false; } @Transactional public int mapProductToOwners(Product product, Owner... owners) { int count = 0; for (Owner owner : owners) { if (this.mapProductToOwner(product, owner)) { ++count; } } return count; } @Transactional public int mapOwnerToProducts(Owner owner, Product... products) { int count = 0; for (Product product : products) { if (this.mapProductToOwner(product, owner)) { ++count; } } return count; } @Transactional public boolean removeOwnerFromProduct(Product product, Owner owner) { String jpql = "DELETE FROM OwnerProduct op " + "WHERE op.product.uuid = :product_uuid AND op.owner.id = :owner_id"; int rows = this.getEntityManager() .createQuery(jpql) .setParameter("owner_id", owner.getId()) .setParameter("product_uuid", product.getUuid()) .executeUpdate(); return rows > 0; } @Transactional public int clearOwnersForProduct(Product product) { String jpql = "DELETE FROM OwnerProduct op " + "WHERE op.product.uuid = :product_uuid"; return this.getEntityManager() .createQuery(jpql) .setParameter("product_uuid", product.getUuid()) .executeUpdate(); } @Transactional public int clearProductsForOwner(Owner owner) { String jpql = "DELETE FROM OwnerProduct op " + "WHERE op.owner.id = :owner_id"; return this.getEntityManager() .createQuery(jpql) .setParameter("owner_id", owner.getId()) .executeUpdate(); } /** * Builds a query which can be used to fetch the current collection of orphaned products. Due * to the nature of this request, it is highly advised that this query be run within a * transaction, with a pessimistic lock mode set. * * @return * A CandlepinQuery for fetching the orphaned products */ public CandlepinQuery<Product> getOrphanedProducts() { // As with many of the owner=>product lookups, we have to do this in two queries. Since // we need to start from product and do a left join back to owner products, we have to use // a native query instead of any of the ORM query languages String sql = "SELECT p.uuid " + "FROM cp2_products p LEFT JOIN cp2_owner_products op ON p.uuid = op.product_uuid " + "WHERE op.owner_id IS NULL"; List<String> uuids = this.getEntityManager() .createNativeQuery(sql) .getResultList(); if (uuids != null && !uuids.isEmpty()) { DetachedCriteria criteria = DetachedCriteria.forClass(Product.class) .add(CPRestrictions.in("uuid", uuids)) .addOrder(Order.asc("uuid")); return this.cpQueryFactory.<Product>buildQuery(this.currentSession(), criteria); } return this.cpQueryFactory.<Product>buildQuery(); } /** * Retrieves a criteria which can be used to fetch a list of products with the specified Red Hat * product ID and entity version belonging to owners other than the owner provided. If no * products were found matching the given criteria, this method returns an empty list. * * @param owner * The owner whose products should be excluded from the results. If an owner is not provided, * no additional filtering will be performed. * * @param productVersions * A mapping of Red Hat product IDs to product versions to fetch * * @return * a criteria for fetching products by version */ @SuppressWarnings("checkstyle:indentation") public CandlepinQuery<Product> getProductsByVersions(Owner owner, Map<String, Integer> productVersions) { if (productVersions == null || productVersions.isEmpty()) { return this.cpQueryFactory.<Product>buildQuery(); } // Impl note: // We perform this operation with two queries here to optimize out some unnecessary queries // when pulling product information. Even when pulling products in a batch, Hibernate will // pull the product collections (attributes, content and dependent product IDs) as separate // query for each product (ugh). By breaking this into two queries -- one for getting the // product UUIDs and one for pulling the actual products -- we will save upwards of two DB // hits per product filtered. We will lose time in the cases where we don't filter any // products, or the products we filter don't have any data in their collections; but we're // only using one additional query in those cases, versus (0-2)n in the normal case. Disjunction disjunction = Restrictions.disjunction(); Criteria uuidCriteria = this.createSecureCriteria("op") .createAlias("op.product", "p") .add(disjunction) .setProjection(Projections.distinct(Projections.property("p.uuid"))); for (Map.Entry<String, Integer> entry : productVersions.entrySet()) { disjunction.add(Restrictions.and( Restrictions.eq("p.id", entry.getKey()), Restrictions.eq("p.entityVersion", entry.getValue()) )); } if (owner != null) { uuidCriteria.add(Restrictions.not(Restrictions.eq("op.owner", owner))); } List<String> uuids = uuidCriteria.list(); if (uuids != null && !uuids.isEmpty()) { DetachedCriteria criteria = this.createSecureDetachedCriteria(Product.class, null) .add(CPRestrictions.in("uuid", uuids)); return this.cpQueryFactory.<Product>buildQuery(this.currentSession(), criteria); } return this.cpQueryFactory.<Product>buildQuery(); } /** * Updates the product references currently pointing to the original product to instead point to * the updated product for the specified owners. * <p/></p> * <strong>Warning:</strong> Hibernate does not gracefully handle situations where the data * backing an entity changes via direct SQL or other outside influence. While, logically, a * refresh on the entity should resolve any divergence, in many cases it does not or causes * errors. As such, whenever this method is called, any active ActivationKey or Pool entities * should be manually evicted from the session and re-queried to ensure they will not clobber * the changes made by this method on persist, nor trigger any errors on refresh. * * @param owner * The owners for which to apply the reference changes * * @param productUuidMap * A mapping of source product UUIDs to updated product UUIDs */ @Transactional public void updateOwnerProductReferences(Owner owner, Map<String, String> productUuidMap) { // Impl note: // We're doing this in straight SQL because direct use of the ORM would require querying all // of these objects and the available HQL refuses to do any joining (implicit or otherwise), // which prevents it from updating collections backed by a join table. // As an added bonus, it's quicker, but we'll have to be mindful of the memory vs backend // state divergence. if (productUuidMap == null || productUuidMap.isEmpty()) { // Nothing to update return; } Session session = this.currentSession(); Map<String, Object> criteria = new HashMap<String, Object>(); Map<Object, Object> uuidMap = Map.class.cast(productUuidMap); criteria.put("product_uuid", productUuidMap.keySet()); criteria.put("owner_id", owner.getId()); // Owner products int count = this.bulkSQLUpdate(OwnerProduct.DB_TABLE, "product_uuid", uuidMap, criteria); log.debug("{} owner-product relations updated", count); // pool provided and derived products count = this.bulkSQLUpdate(Pool.DB_TABLE, "product_uuid", uuidMap, criteria); criteria.remove("product_uuid"); criteria.put("derived_product_uuid", productUuidMap.keySet()); count += this.bulkSQLUpdate(Pool.DB_TABLE, "derived_product_uuid", uuidMap, criteria); log.debug("{} pools updated", count); // pool provided products List<String> ids = session.createSQLQuery("SELECT id FROM cp_pool WHERE owner_id = ?1") .setParameter("1", owner.getId()) .list(); if (ids != null && !ids.isEmpty()) { criteria.clear(); criteria.put("product_uuid", productUuidMap.keySet()); criteria.put("pool_id", ids); count = this.bulkSQLUpdate("cp2_pool_provided_products", "product_uuid", uuidMap, criteria); log.debug("{} provided products updated", count); count = this.bulkSQLUpdate("cp2_pool_derprov_products", "product_uuid", uuidMap, criteria); log.debug("{} derived provided products updated", count); } else { log.debug("0 provided products updated"); log.debug("0 derived provided products updated"); } // Activation key products ids = session.createSQLQuery("SELECT id FROM cp_activation_key WHERE owner_id = ?1") .setParameter("1", owner.getId()) .list(); if (ids != null && !ids.isEmpty()) { criteria.clear(); criteria.put("product_uuid", productUuidMap.keySet()); criteria.put("key_id", ids); count = this.bulkSQLUpdate("cp2_activation_key_products", "product_uuid", uuidMap, criteria); log.debug("{} activation keys updated", count); } else { log.debug("0 activation keys updated"); } // product certificates // Looks like we don't need to do anything here, since we generate them on request. By // leaving them alone, they'll be generated as needed and we save some overhead here. } /** * Removes the product references currently pointing to the specified product for the given * owners. This method cannot be used to remove references to products which are still mapped * to pools. Attempting to do so will result in an IllegalStateException. * <p/></p> * <strong>Warning:</strong> Hibernate does not gracefully handle situations where the data * backing an entity changes via direct SQL or other outside influence. While, logically, a * refresh on the entity should resolve any divergence, in many cases it does not or causes * errors. As such, whenever this method is called, any active ActivationKey entities should * be manually evicted from the session and re-queried to ensure they will not clobber the * changes made by this method on persist, nor trigger any errors on refresh. * * @param owner * The owners for which to apply the reference changes * * @param productUuids * The UUIDs of the products for which to remove references * * @throws IllegalStateException * if the any of the products are in use by one or more pools owned by the given owner */ @Transactional @SuppressWarnings("checkstyle:indentation") public void removeOwnerProductReferences(Owner owner, Collection<String> productUuids) { // Impl note: // We're doing this in straight SQL because direct use of the ORM would require querying all // of these objects and the available HQL refuses to do any joining (implicit or otherwise), // which prevents it from updating collections backed by a join table. // As an added bonus, it's quicker, but we'll have to be mindful of the memory vs backend // state divergence. // Impl note: // We have a restriction in removeProduct which should prevent a product from being removed // from an owner if it is being used by a pool. As such, we shouldn't need to manually clean // the pool tables here. if (productUuids != null && !productUuids.isEmpty()) { log.info("Removing owner-product references for owner: {}, {}", owner, productUuids); Session session = this.currentSession(); // Ensure we aren't trying to remove product references for products still used by // pools for this owner Long poolCount = (Long) session.createCriteria(Pool.class) .createAlias("providedProducts", "providedProd", JoinType.LEFT_OUTER_JOIN) .createAlias("derivedProvidedProducts", "derivedProvidedProd", JoinType.LEFT_OUTER_JOIN) .add(Restrictions.eq("owner", owner)) .add(Restrictions.or( CPRestrictions.in("product.uuid", productUuids), CPRestrictions.in("derivedProduct.uuid", productUuids), CPRestrictions.in("providedProd.uuid", productUuids), CPRestrictions.in("derivedProvidedProd.uuid", productUuids))) .setProjection(Projections.count("id")) .uniqueResult(); if (poolCount != null && poolCount.longValue() > 0) { throw new IllegalStateException( "One or more products are currently used by one or more pools"); } // Owner products //////////////////////////////// Map<String, Object> criteria = new HashMap<String, Object>(); criteria.put("product_uuid", productUuids); criteria.put("owner_id", owner.getId()); int count = this.bulkSQLDelete(OwnerProduct.DB_TABLE, criteria); log.info("{} owner-product relations removed", count); // Impl note: // Even though there's a valid argument to be made here to do so, we do not unlink // content from a product. This may cause headaches in the future when a product object // is unlinked from an owner, but one or more of its contents are not. // Activation key products /////////////////////// String sql = "SELECT id FROM " + ActivationKey.DB_TABLE + " WHERE owner_id = ?1"; List<String> ids = session.createSQLQuery(sql) .setParameter("1", owner.getId()) .list(); if (ids != null && !ids.isEmpty()) { criteria.clear(); criteria.put("key_id", ids); criteria.put("product_uuid", productUuids); count = this.bulkSQLDelete("cp2_activation_key_products", criteria); log.info("{} activation key products removed", count); } else { log.info("0 activation key products removed"); } } } }