/**
* 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 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The OwnerContentCurator provides functionality for managing the mapping between owners and
* content.
*/
public class OwnerContentCurator extends AbstractHibernateCurator<OwnerContent> {
private static Logger log = LoggerFactory.getLogger(OwnerContentCurator.class);
/**
* Default constructor
*/
public OwnerContentCurator() {
super(OwnerContent.class);
}
public Content getContentById(Owner owner, String contentId) {
return this.getContentById(owner.getId(), contentId);
}
@Transactional
public Content getContentById(String ownerId, String contentId) {
return (Content) this.createSecureCriteria()
.createAlias("owner", "owner")
.createAlias("content", "content")
.setProjection(Projections.property("content"))
.add(Restrictions.eq("owner.id", ownerId))
.add(Restrictions.eq("content.id", contentId))
.uniqueResult();
}
public CandlepinQuery<Owner> getOwnersByContent(Content content) {
return this.getOwnersByContent(content.getId());
}
public CandlepinQuery<Owner> getOwnersByContent(String contentId) {
// 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 oc.owner.id FROM OwnerContent oc WHERE oc.content.id = :content_id";
List<String> ids = this.getEntityManager()
.createQuery(jpql, String.class)
.setParameter("content_id", contentId)
.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 content UUIDs currently mapped to the given owner. If the owner is
* not mapped to any content, an empty collection will be returned.
*
* @param owner
* The owner for which to fetch content UUIDs
*
* @return
* a collection of content UUIDs belonging to the given owner
*/
public Collection<String> getContentUuidsByOwner(Owner owner) {
return this.getContentUuidsByOwner(owner.getId());
}
/**
* Fetches a collection of content UUIDs currently mapped to the given owner. If the owner is
* not mapped to any content, an empty collection will be returned.
*
* @param ownerId
* The ID of the owner for which to fetch content UUIDs
*
* @return
* a collection of content UUIDs belonging to the given owner
*/
public Collection<String> getContentUuidsByOwner(String ownerId) {
String jpql = "SELECT oc.content.uuid FROM OwnerContent oc WHERE oc.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 content currently mapped to the given owner.
*
* @param owner
* The owner for which to fetch content
*
* @return
* a query for fetching the content belonging to the given owner
*/
public CandlepinQuery<Content> getContentByOwner(Owner owner) {
return this.getContentByOwner(owner.getId());
}
/**
* Builds a query for fetching the content currently mapped to the given owner.
*
* @param ownerId
* The ID of the owner for which to fetch content
*
* @return
* a query for fetching the content belonging to the given owner
*/
public CandlepinQuery<Content> getContentByOwner(String ownerId) {
// Impl note: See getOwnersByContent for details on why we're doing this in two queries
Collection<String> uuids = this.getContentUuidsByOwner(ownerId);
if (!uuids.isEmpty()) {
DetachedCriteria criteria = this.createSecureDetachedCriteria(Content.class, null)
.add(CPRestrictions.in("uuid", uuids));
return this.cpQueryFactory.<Content>buildQuery(this.currentSession(), criteria);
}
return this.cpQueryFactory.<Content>buildQuery();
}
public CandlepinQuery<Content> getContentByIds(Owner owner, Collection<String> contentIds) {
return this.getContentByIds(owner.getId(), contentIds);
}
public CandlepinQuery<Content> getContentByIds(String ownerId, Collection<String> contentIds) {
if (contentIds == null || contentIds.isEmpty()) {
return this.cpQueryFactory.<Content>buildQuery();
}
// Impl note: See getOwnersByContent for details on why we're doing this in two queries
String jpql = "SELECT oc.content.uuid FROM OwnerContent oc WHERE oc.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(Content.class, null)
.add(CPRestrictions.in("uuid", uuids))
.add(CPRestrictions.in("id", contentIds));
return this.cpQueryFactory.<Content>buildQuery(this.currentSession(), criteria);
}
return this.cpQueryFactory.<Content>buildQuery();
}
@Transactional
public long getOwnerCount(Content content) {
String jpql = "SELECT count(op) FROM OwnerContent op WHERE op.content.uuid = :content_uuid";
long count = (Long) this.getEntityManager()
.createQuery(jpql, Long.class)
.setParameter("content_uuid", content.getUuid())
.getSingleResult();
return count;
}
/**
* Checks if the owner has an existing version of the specified content. This lookup is
* different than the mapping check in that this check will find any content with the
* specified ID, as opposed to checking if a specific version is mapped to the owner.
*
* @param owner
* The owner of the content to lookup
*
* @param contentId
* The Red Hat ID of the content to lookup
*
* @return
* true if the owner has a content with the given RHID; false otherwise
*/
@Transactional
public boolean contentExists(Owner owner, String contentId) {
String jpql = "SELECT count(op) FROM OwnerContent op " +
"WHERE op.owner.id = :owner_id AND op.content.id = :content_id";
long count = (Long) this.getEntityManager()
.createQuery(jpql)
.setParameter("owner_id", owner.getId())
.setParameter("content_id", contentId)
.getSingleResult();
return count > 0;
}
@Transactional
public boolean isContentMappedToOwner(Content content, Owner owner) {
String jpql = "SELECT count(op) FROM OwnerContent op " +
"WHERE op.owner.id = :owner_id AND op.content.uuid = :content_uuid";
long count = (Long) this.getEntityManager()
.createQuery(jpql)
.setParameter("owner_id", owner.getId())
.setParameter("content_uuid", content.getUuid())
.getSingleResult();
return count > 0;
}
@Transactional
public boolean mapContentToOwner(Content content, Owner owner) {
if (!this.isContentMappedToOwner(content, owner)) {
this.create(new OwnerContent(owner, content));
return true;
}
return false;
}
// TODO:
// These pseudo-bulk operations should be updated so they're not flushing after each update.
@Transactional
public int mapContentToOwners(Content content, Owner... owners) {
int count = 0;
for (Owner owner : owners) {
if (this.mapContentToOwner(content, owner)) {
++count;
}
}
return count;
}
@Transactional
public int mapOwnerToContent(Owner owner, Content... content) {
int count = 0;
for (Content elem : content) {
if (this.mapContentToOwner(elem, owner)) {
++count;
}
}
return count;
}
@Transactional
public boolean removeOwnerFromContent(Content content, Owner owner) {
String jpql = "DELETE FROM OwnerContent op " +
"WHERE op.content.uuid = :content_uuid AND op.owner.id = :owner_id";
int rows = this.getEntityManager()
.createQuery(jpql)
.setParameter("owner_id", owner.getId())
.setParameter("content_uuid", content.getUuid())
.executeUpdate();
return rows > 0;
}
@Transactional
public int clearOwnersForContent(Content content) {
String jpql = "DELETE FROM OwnerContent op " +
"WHERE op.content.uuid = :content_uuid";
return this.getEntityManager()
.createQuery(jpql)
.setParameter("content_uuid", content.getUuid())
.executeUpdate();
}
@Transactional
public int clearContentForOwner(Owner owner) {
String jpql = "DELETE FROM OwnerContent op " +
"WHERE op.owner.id = :owner_id";
return this.getEntityManager()
.createQuery(jpql)
.setParameter("owner_id", owner.getId())
.executeUpdate();
}
/**
* Retrieves a criteria which can be used to fetch a list of content with the specified Red Hat
* content ID and entity version belonging to owners other than the owner provided. If no
* content were found matching the given criteria, this method returns an empty list.
*
* @param owner
* The owner whose content should be excluded from the results. If an owner is not provided,
* no additional filtering will be performed.
*
* @param contentVersions
* A mapping of Red Hat content IDs to content versions to fetch
*
* @return
* a criteria for fetching content by version
*/
@SuppressWarnings("checkstyle:indentation")
public CandlepinQuery<Content> getContentByVersions(Owner owner, Map<String, Integer> contentVersions) {
if (contentVersions == null || contentVersions.isEmpty()) {
return this.cpQueryFactory.<Content>buildQuery();
}
// Impl note:
// We perform this operation with two queries here to optimize out some unnecessary queries
// when pulling content information. Even when pulling content in a batch, Hibernate will
// pull the content collections (modified product IDs) as a separate query for each content
// (ugh). By breaking this into two queries -- one for getting the content UUIDs and one
// for pulling the actual content -- we will save upwards of two DB hits per content
// filtered. We will lose time in the cases where we don't filter any content, or the
// content we filter don't have any data in their collections; but we're only using one
// additional query in those cases, versus n additional in the normal case.
Disjunction disjunction = Restrictions.disjunction();
Criteria uuidCriteria = this.createSecureCriteria("oc")
.createAlias("oc.content", "c")
.add(disjunction)
.setProjection(Projections.distinct(Projections.property("c.uuid")));
for (Map.Entry<String, Integer> entry : contentVersions.entrySet()) {
disjunction.add(Restrictions.and(
Restrictions.eq("c.id", entry.getKey()),
Restrictions.eq("c.entityVersion", entry.getValue())
));
}
if (owner != null) {
uuidCriteria.add(Restrictions.not(Restrictions.eq("oc.owner", owner)));
}
List<String> uuids = uuidCriteria.list();
if (uuids != null && !uuids.isEmpty()) {
DetachedCriteria criteria = this.createSecureDetachedCriteria(Content.class, null)
.add(CPRestrictions.in("uuid", uuids));
return this.cpQueryFactory.<Content>buildQuery(this.currentSession(), criteria);
}
return this.cpQueryFactory.<Content>buildQuery();
}
/**
* Builds a query which can be used to fetch the current collection of orphaned content. 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 content
*/
public CandlepinQuery<Content> getOrphanedContent() {
// As with many of the owner=>content lookups, we have to do this in two queries. Since
// we need to start from content and do a left join back to owner content, we have to use
// a native query instead of any of the ORM query languages
String sql = "SELECT c.uuid " +
"FROM cp2_content c LEFT JOIN cp2_owner_content oc ON c.uuid = oc.content_uuid " +
"WHERE oc.owner_id IS NULL";
List<String> uuids = this.getEntityManager()
.createNativeQuery(sql)
.getResultList();
if (uuids != null && !uuids.isEmpty()) {
DetachedCriteria criteria = DetachedCriteria.forClass(Content.class)
.add(CPRestrictions.in("uuid", uuids))
.addOrder(Order.asc("uuid"));
return this.cpQueryFactory.<Content>buildQuery(this.currentSession(), criteria);
}
return this.cpQueryFactory.<Content>buildQuery();
}
/**
* Updates the content references currently pointing to the original content to instead point to
* the updated content for the specified owners.
* <p/></p>
* <strong>Note:</strong> product-content mappings are not modified by this method.
* <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 Environment 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 owner for which to apply the reference changes
*
* @param contentUuidMap
* A mapping of source content UUIDs to updated content UUIDs
*/
@Transactional
public void updateOwnerContentReferences(Owner owner, Map<String, String> contentUuidMap) {
// Impl note:
// We're doing this in straight SQL because direct use of the ORM would require querying all
// of these objects and 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 (contentUuidMap == null || contentUuidMap.isEmpty()) {
// Nothing to update
return;
}
Session session = this.currentSession();
Map<String, Object> criteria = new HashMap<String, Object>();
Map<Object, Object> uuidMap = Map.class.cast(contentUuidMap);
criteria.put("content_uuid", contentUuidMap.keySet());
criteria.put("owner_id", owner.getId());
// Owner content
int count = this.bulkSQLUpdate(OwnerContent.DB_TABLE, "content_uuid", uuidMap, criteria);
log.info("{} owner-content relations updated", count);
// Impl note:
// We're not managing product-content references, since versioning changes require us to
// handle that with more explicit logic. Instead, we rely on the content manager using
// the product manager to fork/update products when a related content entity changes.
// environment content
List<String> ids = session.createSQLQuery("SELECT id FROM cp_environment WHERE owner_id = ?1")
.setParameter("1", owner.getId())
.list();
if (ids != null && !ids.isEmpty()) {
criteria.clear();
criteria.put("environment_id", ids);
criteria.put("content_uuid", contentUuidMap.keySet());
count = this.bulkSQLUpdate(EnvironmentContent.DB_TABLE, "content_uuid", uuidMap, criteria);
log.info("{} environment-content relations updated", count);
}
else {
log.info("0 environment-content relations updated");
}
}
/**
* Removes the content references currently pointing to the specified content for the given
* owners.
* <p/></p>
* <strong>Note:</strong> product-content mappings are not modified by this method.
* <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 Environment 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 owner for which to apply the reference changes
*
* @param contentUuids
* A collection of content UUIDs representing the content entities to orphan
*/
@Transactional
public void removeOwnerContentReferences(Owner owner, Collection<String> contentUuids) {
// Impl note:
// As is the case in updateOwnerContentReferences, HQL's bulk delete doesn't allow us to
// touch anything that even looks like a join. As such, we have to do this in vanilla SQL.
if (contentUuids != null && !contentUuids.isEmpty()) {
log.info("Removing owner-content references for owner: {}, {}", owner, contentUuids);
Session session = this.currentSession();
// Owner content
Map<String, Object> criteria = new HashMap<String, Object>();
criteria.put("owner_id", owner.getId());
criteria.put("content_uuid", contentUuids);
int count = this.bulkSQLDelete(OwnerContent.DB_TABLE, criteria);
log.info("{} owner-content relations removed", count);
// Impl note:
// We're not managing product-content references, since versioning changes require us to
// handle that with more explicit logic. Instead, we rely on the content manager using
// the product manager to fork/update products when a related content entity changes.
// environment content
String sql = "SELECT id FROM " + Environment.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("environment_id", ids);
criteria.put("content_uuid", contentUuids);
count = this.bulkSQLDelete(EnvironmentContent.DB_TABLE, criteria);
log.info("{} environment-content relations updated", count);
}
else {
log.info("0 environment-content relations updated");
}
}
}
}