/**
* 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.cache.CandlepinCache;
import org.candlepin.common.config.Configuration;
import org.candlepin.util.AttributeValidator;
import com.google.inject.Inject;
import com.google.inject.persist.Transactional;
import org.hibernate.Criteria;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.sql.JoinType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xnap.commons.i18n.I18n;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.cache.Cache;
import javax.persistence.TypedQuery;
/**
* interact with Products.
*/
public class ProductCurator extends AbstractHibernateCurator<Product> {
private static Logger log = LoggerFactory.getLogger(ProductCurator.class);
private Configuration config;
private I18n i18n;
private CandlepinCache candlepinCache;
private AttributeValidator attributeValidator;
/**
* default ctor
*/
@Inject
public ProductCurator(Configuration config, I18n i18n, CandlepinCache candlepinCache,
AttributeValidator attributeValidator) {
super(Product.class);
this.config = config;
this.i18n = i18n;
this.candlepinCache = candlepinCache;
this.attributeValidator = attributeValidator;
}
/**
* Retrieves a Product instance for the product with the specified name. If a matching product
* could not be found, this method returns null.
*
* @param owner
* The owner/org in which to search for a product
*
* @param name
* The name of the product to retrieve
*
* @return
* a Product instance for the product with the specified name, or null if a matching product
* was not found.
*/
public Product lookupByName(Owner owner, String name) {
return (Product) this.createSecureCriteria(OwnerProduct.class, null)
.createAlias("owner", "owner")
.createAlias("product", "product")
.setProjection(Projections.property("product"))
.add(Restrictions.eq("owner.id", owner.getId()))
.add(Restrictions.eq("product.name", name))
.uniqueResult();
}
public CandlepinQuery<Product> listAllByUuids(Collection<? extends Serializable> uuids) {
DetachedCriteria criteria = this.createSecureDetachedCriteria()
.add(CPRestrictions.in("uuid", uuids));
return this.cpQueryFactory.<Product>buildQuery(this.currentSession(), criteria);
}
public Set<Product> getPoolDerivedProvidedProductsCached(Pool pool) {
return getPoolDerivedProvidedProductsCached(pool.getId());
}
public Set<Product> getPoolDerivedProvidedProductsCached(String poolId) {
Set<String> uuids = getDerivedPoolProvidedProductUuids(poolId);
return getProductsByUuidCached(uuids);
}
public Set<Product> getPoolProvidedProductsCached(Pool pool) {
return getPoolProvidedProductsCached(pool.getId());
}
public Set<Product> getPoolProvidedProductsCached(String poolId) {
Set<String> providedUuids = getPoolProvidedProductUuids(poolId);
return getProductsByUuidCached(providedUuids);
}
/**
* Finds all provided products for a given poolId
*
* @param poolId
* @return Set of UUIDs
*/
public Set<String> getPoolProvidedProductUuids(String poolId) {
TypedQuery<String> query = getEntityManager().createQuery(
"SELECT product.uuid FROM Pool p INNER JOIN p.providedProducts product where p.id = :poolid",
String.class);
query.setParameter("poolid", poolId);
return new HashSet<String>(query.getResultList());
}
/**
* Finds all derived provided products for a given poolId
*
* @param poolId
* @return Set of UUIDs
*/
public Set<String> getDerivedPoolProvidedProductUuids(String poolId) {
TypedQuery<String> query = getEntityManager().createQuery(
"SELECT product.uuid FROM Pool p INNER JOIN p.derivedProvidedProducts product " +
"WHERE p.id = :poolid",
String.class);
query.setParameter("poolid", poolId);
return new HashSet<String>(query.getResultList());
}
/**
* Gets products by Id from JCache or database
*
* The retrieved objects are fully hydrated. If an entity is not present in the cache,
* then it is retrieved them from the database and is fully hydrated
*
* @param productUuids
* @return Fully hydrated Product objects
*/
public Set<Product> getProductsByUuidCached(Set<String> productUuids) {
if (productUuids.size() == 0) {
return new HashSet<Product>();
}
Set<Product> products = new HashSet<Product>();
Set<String> notInCache = new HashSet<String>();
Cache<String, Product> productCache = candlepinCache.getProductCache();
//First find all products that are in cache. Those keys that
//are not present in the cache will not be in the result Map
Map<String, Product> productsFromCache = productCache.getAll(productUuids);
products.addAll(productsFromCache.values());
notInCache.addAll(productUuids);
notInCache.removeAll(productsFromCache.keySet());
//Now find, hydrate and cache all the products that has been
//missing in the cache
Map<String, Product> freshProducts = getHydratedProductsByUuid(notInCache);
productCache.putAll(freshProducts);
products.addAll(freshProducts.values());
return products;
}
/**
* Loads the set of products from database and triggers all lazy loads.
* @param uuids
* @return Map of UUID to Product instance
*/
public Map<String, Product> getHydratedProductsByUuid(Set<String> uuids) {
if (uuids == null || uuids.isEmpty()) {
return new HashMap<String, Product>();
}
Map<String, Product> productsByUuid = new HashMap<String, Product>();
for (Product p : this.listAllByUuids(uuids)) {
if (p == null) {
continue;
}
p.getAttributes().size();
for (ProductContent cont : p.getProductContent()) {
cont.getContent().getModifiedProductIds().size();
}
p.getDependentProductIds().size();
productsByUuid.put(p.getUuid(), p);
}
return productsByUuid;
}
/**
* Validates and corrects the object references maintained by the given product instance.
*
* @param entity
* The product entity to validate
*
* @return
* The provided product reference
*/
protected Product validateProductReferences(Product entity) {
for (Map.Entry<String, String> entry : entity.getAttributes().entrySet()) {
this.attributeValidator.validate(entry.getKey(), entry.getValue());
}
for (ProductContent pc : entity.getProductContent()) {
pc.setProduct(entity);
}
// TODO: Add more reference checks here.
return entity;
}
@Transactional
public Product create(Product entity) {
log.debug("Persisting new product entity: {}", entity);
this.validateProductReferences(entity);
return super.create(entity);
}
@Transactional
public Product merge(Product entity) {
log.debug("Merging product entity: {}", entity);
this.validateProductReferences(entity);
return super.merge(entity);
}
// Needs an override due to the use of UUID as db identifier.
@Override
@Transactional
public void delete(Product entity) {
Product toDelete = find(entity.getUuid());
currentSession().delete(toDelete);
}
/**
* Checks if any of the provided product is linked to one or more pools for the given owner.
*
* @param owner
* The owner to use for finding pools/subscriptions
*
* @param product
* The product to check for subscriptions
*
* @return
* true if the product is linked to one or more subscriptions; false otherwise.
*/
public boolean productHasSubscriptions(Owner owner, Product product) {
return ((Long) currentSession().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(
Restrictions.eq("product.uuid", product.getUuid()),
Restrictions.eq("derivedProduct.uuid", product.getUuid()),
Restrictions.eq("providedProd.uuid", product.getUuid()),
Restrictions.eq("derivedProvidedProd.uuid", product.getUuid())))
.setProjection(Projections.count("id"))
.uniqueResult()) > 0;
}
public CandlepinQuery<Product> getProductsByContent(Owner owner, Collection<String> contentIds) {
return this.getProductsByContent(owner, contentIds, null);
}
@SuppressWarnings("unchecked")
public CandlepinQuery<Product> getProductsByContent(Owner owner, Collection<String> contentIds,
Collection<String> productsToOmit) {
if (owner != null && contentIds != null && !contentIds.isEmpty()) {
// Impl note:
// We have to break this up into two queries for proper cursor and pagination support.
// Hibernate currently has two nasty "features" which break these in their own special
// way:
// - Distinct, when applied in any way outside of direct SQL, happens in Hibernate
// *after* the results are pulled down, if and only if the results are fetched as a
// list. The filtering does not happen when the results are fetched with a cursor.
// - Because result limiting (first+last result specifications) happens at the query
// level and distinct filtering does not, cursor-based pagination breaks due to
// potential results being removed after a page of results is fetched.
Criteria idCriteria = this.createSecureCriteria(OwnerProduct.class, null)
.createAlias("product", "product")
.createAlias("product.productContent", "pcontent")
.createAlias("pcontent.content", "content")
.createAlias("owner", "owner")
.add(Restrictions.eq("owner.id", owner.getId()))
.add(CPRestrictions.in("content.id", contentIds))
.setProjection(Projections.distinct(Projections.property("product.uuid")));
if (productsToOmit != null && !productsToOmit.isEmpty()) {
idCriteria.add(Restrictions.not(CPRestrictions.in("product.id", productsToOmit)));
}
List<String> productUuids = idCriteria.list();
if (productUuids != null && !productUuids.isEmpty()) {
DetachedCriteria criteria = this.createSecureDetachedCriteria()
.add(CPRestrictions.in("uuid", productUuids));
return this.cpQueryFactory.<Product>buildQuery(this.currentSession(), criteria);
}
}
return this.cpQueryFactory.<Product>buildQuery();
}
@SuppressWarnings("unchecked")
public CandlepinQuery<Product> getProductsByContentUuids(Collection<String> contentUuids) {
if (contentUuids != null && !contentUuids.isEmpty()) {
// See note above in getProductsByContent for details on why we do two queries here
// instead of one.
Criteria idCriteria = this.createSecureCriteria()
.createAlias("productContent", "pcontent")
.createAlias("pcontent.content", "content")
.add(CPRestrictions.in("content.uuid", contentUuids))
.setProjection(Projections.distinct(Projections.id()));
List<String> productUuids = idCriteria.list();
if (productUuids != null && !productUuids.isEmpty()) {
DetachedCriteria criteria = this.createSecureDetachedCriteria()
.add(CPRestrictions.in("uuid", productUuids));
return this.cpQueryFactory.<Product>buildQuery(this.currentSession(), criteria);
}
}
return this.cpQueryFactory.<Product>buildQuery();
}
@SuppressWarnings("unchecked")
public CandlepinQuery<Product> getProductsByContentUuids(Owner owner, Collection<String> contentUuids) {
if (contentUuids != null && !contentUuids.isEmpty()) {
// See note above in getProductsByContent for details on why we do two queries here
// instead of one.
Criteria idCriteria = this.createSecureCriteria(OwnerProduct.class, null)
.createAlias("product", "product")
.createAlias("product.productContent", "pcontent")
.createAlias("pcontent.content", "content")
.createAlias("owner", "owner")
.add(Restrictions.eq("owner.id", owner.getId()))
.add(CPRestrictions.in("content.uuid", contentUuids))
.setProjection(Projections.distinct(Projections.property("product.uuid")));
List<String> productUuids = idCriteria.list();
if (productUuids != null && !productUuids.isEmpty()) {
DetachedCriteria criteria = this.createSecureDetachedCriteria()
.add(CPRestrictions.in("uuid", productUuids));
return this.cpQueryFactory.<Product>buildQuery(this.currentSession(), criteria);
}
}
return this.cpQueryFactory.<Product>buildQuery();
}
}