/**
* 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.dto.ContentData;
import org.candlepin.util.SetView;
import org.candlepin.util.Util;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Type;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
/**
* ProductContent
*/
@XmlRootElement
@XmlAccessorType(XmlAccessType.PROPERTY)
@Entity
@Table(name = Content.DB_TABLE)
public class Content extends AbstractHibernateObject implements SharedEntity, Cloneable {
/** Name of the table backing this object in the database */
public static final String DB_TABLE = "cp2_content";
// Object ID
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid", strategy = "uuid")
@NotNull
private String uuid;
// Internal RH content ID
@Column(name = "content_id")
@Size(max = 32)
@NotNull
private String id;
@Column(nullable = false)
@Size(max = 255)
@NotNull
private String type;
@Column(nullable = false, unique = true)
@Size(max = 255)
@NotNull
private String label;
@Column(nullable = false)
@Size(max = 255)
@NotNull
private String name;
@Column(nullable = false)
@Size(max = 255)
@NotNull
private String vendor;
@Column(nullable = true)
@Size(max = 255)
private String contentUrl;
@Column(nullable = true)
@Size(max = 255)
private String requiredTags;
// for selecting Y/Z stream
@Column(nullable = true)
@Size(max = 255)
private String releaseVer;
// attribute?
@Column(nullable = true)
@Size(max = 255)
private String gpgUrl;
@Column(nullable = true)
private Long metadataExpire;
@BatchSize(size = 128)
@ElementCollection
@CollectionTable(name = "cp2_content_modified_products", joinColumns = @JoinColumn(name = "content_uuid"))
@Column(name = "element")
@Size(max = 255)
private Set<String> modifiedProductIds;
@Column(nullable = true)
@Size(max = 255)
private String arches;
@XmlTransient
@Column(name = "entity_version")
private Integer entityVersion;
@XmlTransient
@Column
@Type(type = "org.hibernate.type.NumericBooleanType")
private boolean locked;
/**
* Default constructor
*/
public Content() {
this.modifiedProductIds = new HashSet<String>();
}
/**
* ID-based constructor so API users can specify an ID in place of a full object.
*
* @param id
* The ID for this content
*/
public Content(String id) {
this();
this.setId(id);
}
public Content(String id, String name, String type, String label, String vendor) {
this(id);
this.setName(name);
this.setType(type);
this.setLabel(label);
this.setVendor(vendor);
}
/**
* Creates a shallow copy of the specified source content. Attributes and content are not
* duplicated, but the joining objects are (ContentAttribute, ContentContent, etc.).
* <p></p>
* Unlike the merge method, all properties from the source content are copied, including the
* state of any null collections and any identifier fields.
*
* @param source
* The Content instance to copy
*/
protected Content(Content source) {
this.setUuid(source.getUuid());
this.setId(source.getId());
this.setCreated(source.getCreated() != null ? (Date) source.getCreated().clone() : null);
this.merge(source);
}
/**
* Copies several properties from the given content on to this content instance. Properties that
* are not copied over include any identifiying fields (UUID, ID), the creation date and locking
* states. Values on the source content which are null will be ignored.
*
* @param source
* The source content instance from which to pull content information
*
* @return
* this content instance
*/
public Content merge(Content source) {
this.setUpdated(source.getUpdated() != null ? (Date) source.getUpdated().clone() : null);
this.setType(source.getType());
this.setLabel(source.getLabel());
this.setName(source.getName());
this.setVendor(source.getVendor());
this.setContentUrl(source.getContentUrl());
this.setRequiredTags(source.getRequiredTags());
this.setReleaseVersion(source.getReleaseVersion());
this.setGpgUrl(source.getGpgUrl());
this.setMetadataExpire(source.getMetadataExpire());
this.setArches(source.getArches());
this.setModifiedProductIds(source.getModifiedProductIds());
return this;
}
@Override
public Content clone() {
Content copy;
try {
copy = (Content) super.clone();
}
catch (CloneNotSupportedException e) {
// This should never happen.
throw new RuntimeException("Clone not supported", e);
}
// Impl note:
// In most cases, our collection setters copy the contents of the input collections to their
// own internal collections, so we don't need to worry about our two instances sharing a
// collection.
copy.modifiedProductIds = new HashSet<String>();
copy.setModifiedProductIds(this.getModifiedProductIds());
copy.setCreated(this.getCreated() != null ? (Date) this.getCreated().clone() : null);
copy.setUpdated(this.getUpdated() != null ? (Date) this.getUpdated().clone() : null);
return copy;
}
/**
* Returns a DTO representing this entity.
*
* @return
* a DTO representing this entity
*/
public ContentData toDTO() {
return new ContentData(this);
}
/**
* Retrieves this content's object/database UUID. While the content ID may exist multiple times
* in the database (if in use by multiple owners), this UUID uniquely identifies a
* content instance.
*
* @return
* this content's database UUID.
*/
public String getUuid() {
return uuid;
}
/**
* Sets this content's object/database ID. Note that this ID is used to uniquely identify this
* particular object and has no baring on the Red Hat content ID.
*
* @param uuid
* The object ID to assign to this content.
*/
public void setUuid(String uuid) {
this.uuid = uuid;
}
/**
* Retrieves this content's ID. Assigned by the content provider, and may exist in
* multiple owners, thus may not be unique in itself.
*
* @return
* this content's ID.
*/
public String getId() {
return this.id;
}
/**
* Sets the content ID for this content. The content ID is the Red Hat content ID and should not
* be confused with the object ID.
*
* @param id
* The new content ID for this content.
*/
public void setId(String id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getVendor() {
return vendor;
}
public void setVendor(String vendor) {
this.vendor = vendor;
}
public String getContentUrl() {
return contentUrl;
}
public void setContentUrl(String contentUrl) {
this.contentUrl = contentUrl;
}
/**
* @return Comma separated list of tags this content set requires to be
* enabled.
*/
public String getRequiredTags() {
return requiredTags;
}
/**
* @param requiredTags Comma separated list of tags this content set
* requires.
*/
public void setRequiredTags(String requiredTags) {
this.requiredTags = requiredTags;
}
/**
* @return the releaseVer
*/
public String getReleaseVersion() {
return releaseVer;
}
/**
* @param releaseVer the releaseVer to set
*/
public void setReleaseVersion(String releaseVer) {
this.releaseVer = releaseVer;
}
public String getGpgUrl() {
return gpgUrl;
}
public void setGpgUrl(String gpgUrl) {
this.gpgUrl = gpgUrl;
}
public Long getMetadataExpire() {
return metadataExpire;
}
public void setMetadataExpire(Long metadataExpire) {
this.metadataExpire = metadataExpire;
}
/**
* Retrieves the collection of IDs representing products that are modified by this content. If
* the modified product IDs have not yet been defined, this method returns an empty collection.
*
* @return
* the modified product IDs of the content
*/
public Set<String> getModifiedProductIds() {
return new SetView(this.modifiedProductIds);
}
/**
* Adds the specified product ID as a product ID to be modified by this content. If the product
* ID is already modified in by this content, it will not be added again.
*
* @param productId
* The product ID to add as a modified product ID to this content
*
* @throws IllegalArgumentException
* if productId is null
*
* @return
* true if the product ID was added successfully; false otherwise
*/
public boolean addModifiedProductId(String productId) {
if (productId == null) {
throw new IllegalArgumentException("productId is null");
}
return this.modifiedProductIds.add(productId);
}
/**
* Removes the specified product ID from the collection of product IDs to be modified by the
* content represented by this content. If the product ID is not modified by this content, this
* method does nothing.
*
* @param productId
* The product ID to remove from the modified product IDs on this content
*
* @throws IllegalArgumentException
* if productId is null
*
* @return
* true if the product ID was removed successfully; false otherwise
*/
public boolean removeModifiedProductId(String productId) {
if (productId == null) {
throw new IllegalArgumentException("productId is null");
}
return this.modifiedProductIds != null ? this.modifiedProductIds.remove(productId) : false;
}
/**
* Sets the modified product IDs for this content. Any previously existing modified product IDs
* will be cleared before assigning the given product IDs.
*
* @param modifiedProductIds
* A collection of product IDs to be modified by the content content, or null to clear the
* existing modified product IDs
*
* @return
* a reference to this DTO
*/
public Content setModifiedProductIds(Collection<String> modifiedProductIds) {
this.modifiedProductIds.clear();
if (modifiedProductIds != null) {
this.modifiedProductIds.addAll(modifiedProductIds);
}
return this;
}
public void setArches(String arches) {
this.arches = arches;
}
public String getArches() {
return arches;
}
@XmlTransient
@JsonIgnore
public Content setLocked(boolean locked) {
this.locked = locked;
return this;
}
@XmlTransient
public boolean isLocked() {
return this.locked;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other instanceof Content) {
Content that = (Content) other;
boolean equals = new EqualsBuilder()
.append(this.id, that.id)
.append(this.type, that.type)
.append(this.label, that.label)
.append(this.name, that.name)
.append(this.vendor, that.vendor)
.append(this.contentUrl, that.contentUrl)
.append(this.requiredTags, that.requiredTags)
.append(this.releaseVer, that.releaseVer)
.append(this.gpgUrl, that.gpgUrl)
.append(this.metadataExpire, that.metadataExpire)
.append(this.arches, that.arches)
.append(this.locked, that.locked)
.isEquals();
if (equals) {
if (!Util.collectionsAreEqual(this.modifiedProductIds, that.modifiedProductIds)) {
return false;
}
}
return equals;
}
return false;
}
@Override
public int hashCode() {
HashCodeBuilder builder = new HashCodeBuilder(7, 17)
.append(this.id);
return builder.toHashCode();
}
/**
* Calculates and returns a version hash for this entity. This method operates much like the
* hashCode method, except that it is more accurate and should have fewer collisions.
*
* @return
* a version hash for this entity
*/
public int getEntityVersion() {
// This must always be a subset of equals
HashCodeBuilder builder = new HashCodeBuilder(37, 7)
.append(this.id)
.append(this.type)
.append(this.label)
.append(this.name)
.append(this.vendor)
.append(this.contentUrl)
.append(this.requiredTags)
.append(this.releaseVer)
.append(this.gpgUrl)
.append(this.metadataExpire)
.append(this.arches)
.append(this.locked);
// Impl note:
// We need to be certain that the hash code is calculated in a way that's order
// independent and not subject to Hibernate's poor hashCode implementation on proxy
// collections. This calculation follows that defined by the Set.hashCode method.
int accumulator = 0;
if (!this.modifiedProductIds.isEmpty()) {
for (String pid : this.modifiedProductIds) {
accumulator += (pid != null ? pid.hashCode() : 0);
}
builder.append(accumulator);
}
// Return
return builder.toHashCode();
}
/**
* Determines whether or not this entity would be changed if the given DTO were applied to this
* object.
*
* @param dto
* The content DTO to check for changes
*
* @throws IllegalArgumentException
* if dto is null
*
* @return
* true if this content would be changed by the given DTO; false otherwise
*/
public boolean isChangedBy(ContentData dto) {
if (dto == null) {
throw new IllegalArgumentException("dto is null");
}
if (dto.getId() != null && !dto.getId().equals(this.id)) {
return true;
}
if (dto.getType() != null && !dto.getType().equals(this.type)) {
return true;
}
if (dto.getLabel() != null && !dto.getLabel().equals(this.label)) {
return true;
}
if (dto.getName() != null && !dto.getName().equals(this.name)) {
return true;
}
if (dto.getVendor() != null && !dto.getVendor().equals(this.vendor)) {
return true;
}
if (dto.getContentUrl() != null && !dto.getContentUrl().equals(this.contentUrl)) {
return true;
}
if (dto.getRequiredTags() != null && !dto.getRequiredTags().equals(this.requiredTags)) {
return true;
}
if (dto.getReleaseVersion() != null && !dto.getReleaseVersion().equals(this.releaseVer)) {
return true;
}
if (dto.getGpgUrl() != null && !dto.getGpgUrl().equals(this.gpgUrl)) {
return true;
}
if (dto.getMetadataExpire() != null && !dto.getMetadataExpire().equals(this.metadataExpire)) {
return true;
}
if (dto.getArches() != null && !dto.getArches().equals(this.arches)) {
return true;
}
if (dto.isLocked() != null && !dto.isLocked().equals(this.locked)) {
return true;
}
Collection<String> modifiedProductIds = dto.getModifiedProductIds();
if (modifiedProductIds != null &&
!Util.collectionsAreEqual(this.modifiedProductIds, modifiedProductIds)) {
return true;
}
return false;
}
@Override
public String toString() {
return String.format("ContentData [uuid: %s, id: %s, name: %s, label: %s]",
this.uuid, this.id, this.name, this.label);
}
@PrePersist
@PreUpdate
public void updateEntityVersion() {
this.entityVersion = this.getEntityVersion();
}
}