/**
* 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.jackson.CandlepinAttributeDeserializer;
import org.candlepin.jackson.CandlepinLegacyAttributeSerializer;
import org.candlepin.model.dto.ProductContentData;
import org.candlepin.model.dto.ProductData;
import org.candlepin.util.ListView;
import org.candlepin.util.MapView;
import org.candlepin.util.SetView;
import org.candlepin.util.Util;
import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.CascadeType;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import org.hibernate.annotations.Type;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
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.MapKeyColumn;
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;
/**
* Represents a Product that can be consumed and entitled. Products define the
* software or entity they want to entitle i.e. RHEL Server. They also contain
* descriptive meta data that might limit the Product i.e. 4 cores per server
* with 4 guests.
*/
@XmlRootElement
@XmlAccessorType(XmlAccessType.PROPERTY)
@Entity
@Table(name = Product.DB_TABLE)
public class Product extends AbstractHibernateObject implements SharedEntity, Linkable, Cloneable, Eventful {
/** Name of the table backing this object in the database */
public static final String DB_TABLE = "cp2_products";
/**
* Commonly used/recognized product attributes
*/
public static final class Attributes {
/** Attribute to specify the architecture on which a given product can be installed/run; may be set
* to the value "ALL" to specify all architectures */
public static final String ARCHITECTURE = "arch";
/** Attribute for specifying the type of branding to apply to a marketing product (SKU) */
public static final String BRANDING_TYPE = "brand_type";
/** Attribute for enabling content overrides */
public static final String CONTENT_OVERRIDE_ENABLED = "content_override_enabled";
/** Attribute for disabling content overrides */
public static final String CONTENT_OVERRIDE_DISABLED = "content_override_disabled";
/** Attribute specifying the number of cores that can be covered by an entitlement using the SKU */
public static final String CORES = "cores";
/** Attribute specifying the maximum number of guests that can be covered by an entitlement using
* this SKU. -1 specifies no limit */
public static final String GUEST_LIMIT = "guest_limit";
/** Attribute specifying whether or not derived pools created for a given product will be
* host-limited */
public static final String HOST_LIMITED = "host_limited";
/** Attribute used to specify the instance multiplier for a pool. When specified, pools using the
* product will use instance-based subscriptions, multiplying the size of the pool, but consuming
* multiples of this value for each physical bind. */
public static final String INSTANCE_MULTIPLIER = "instance_multiplier";
/** Attribute specifying whether or not management is enabled for a given product; passed down to the
* certificate */
public static final String MANAGEMENT_ENABLED = "management_enabled";
/** Attribute specifying the amount of RAM that can be covered by an entitlement using the SKU */
public static final String RAM = "ram";
/** Attribute specifying the number of sockets that can be covered by an entitlement using the SKU */
public static final String SOCKETS = "sockets";
/** Attribute used to identify stacked products and pools */
public static final String STACKING_ID = "stacking_id";
/** Attribute for specifying the provided support level provided by a given product */
public static final String SUPPORT_LEVEL = "support_level";
/** Attribute for specifying if the product is exempt from the support level */
public static final String SUPPORT_LEVEL_EXEMPT = "support_level_exempt";
/** Attribute providing a human-readable description of the support type; passed to the certificate */
public static final String SUPPORT_TYPE = "support_type";
/** Attribute for specifying the TTL of a product, in days */
public static final String TTL = "expires_after";
/** Attribute representing the product type; passed down to the certificate */
public static final String TYPE = "type";
/** Attribute representing the product variant; passed down to the certificate */
public static final String VARIANT = "variant";
/** Attribute specifying the number of virtual CPUs that can be covered by an entitlement using
* this SKU */
public static final String VCPU = "vcpu";
/** Attribute for specifying the number of guests that can use a given product */
public static final String VIRT_LIMIT = "virt_limit";
/** Attribute for specifying the product is only available to guests */
public static final String VIRT_ONLY = "virt_only";
/** Attribute to specify the version of a product */
public static final String VERSION = "version";
/** Attribute specifying the number of days prior to expiration the client should be warned about an
* expiring subscription for a given product */
public static final String WARNING_PERIOD = "warning_period";
}
// Object ID
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid", strategy = "uuid")
@NotNull
private String uuid;
// Internal RH product ID,
@Column(name = "product_id")
@NotNull
private String id;
@Column(nullable = false)
@Size(max = 255)
@NotNull
private String name;
/**
* How many entitlements per quantity
*/
@Column
private Long multiplier;
@ElementCollection
@BatchSize(size = 32)
@CollectionTable(name = "cp2_product_attributes", joinColumns = @JoinColumn(name = "product_uuid"))
@MapKeyColumn(name = "name")
@Column(name = "value")
@Cascade({ CascadeType.ALL })
@Fetch(FetchMode.SUBSELECT)
@JsonSerialize(using = CandlepinLegacyAttributeSerializer.class)
@JsonDeserialize(using = CandlepinAttributeDeserializer.class)
private Map<String, String> attributes;
@ElementCollection
@BatchSize(size = 32)
@CollectionTable(name = "cp2_product_content", joinColumns = @JoinColumn(name = "product_uuid"))
@Column(name = "content_uuid")
@LazyCollection(LazyCollectionOption.EXTRA) // allows .size() without loading all data
private List<ProductContent> productContent;
/*
* hibernate persists empty set as null, and tries to fetch
* dependentProductIds upon a fetch when we lazy load. to fix this, we eager
* fetch.
*/
@ElementCollection
@CollectionTable(name = "cp2_product_dependent_products",
joinColumns = @JoinColumn(name = "product_uuid"))
@Column(name = "element")
@BatchSize(size = 32)
@LazyCollection(LazyCollectionOption.FALSE)
private Set<String> dependentProductIds;
@XmlTransient
@Column(name = "entity_version")
private Integer entityVersion;
@XmlTransient
@Column
@Type(type = "org.hibernate.type.NumericBooleanType")
private boolean locked;
public Product() {
this.attributes = new HashMap<String, String>();
this.productContent = new LinkedList<ProductContent>();
this.dependentProductIds = new HashSet<String>();
}
/**
* Constructor Use this variant when creating a new object to persist.
*
* @param productId The Red Hat product ID for the new product.
* @param name Human readable Product name
*/
public Product(String productId, String name) {
this();
this.setId(productId);
this.setName(name);
}
public Product(String productId, String name, Long multiplier) {
this(productId, name);
this.setMultiplier(multiplier);
}
public Product(String productId, String name, String variant, String version, String arch, String type) {
this(productId, name, 1L);
this.setAttribute(Attributes.VERSION, version);
this.setAttribute(Attributes.VARIANT, variant);
this.setAttribute(Attributes.TYPE, type);
this.setAttribute(Attributes.ARCHITECTURE, arch);
}
/**
* Creates a shallow copy of the specified source product. Owners, attributes and content are
* not duplicated, but the joining objects are (ProductAttribute, ProductContent, etc.).
* <p></p>
* Unlike the merge method, all properties from the source product are copied, including the
* state of any null collections and any identifier fields.
*
* @param source
* The Product instance to copy
*/
public Product(Product source) {
this();
this.setUuid(source.getUuid());
this.setId(source.getId());
// 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.
this.setName(source.getName());
this.setMultiplier(source.getMultiplier());
// Copy attributes
this.setAttributes(source.getAttributes());
// Copy content
List<ProductContent> content = new LinkedList<ProductContent>();
for (ProductContent src : source.getProductContent()) {
ProductContent dest = new ProductContent(this, src.getContent(), src.isEnabled());
dest.setCreated(src.getCreated() != null ? (Date) src.getCreated().clone() : null);
dest.setUpdated(src.getUpdated() != null ? (Date) src.getUpdated().clone() : null);
content.add(dest);
}
this.setProductContent(content);
// Copy dependent product IDs
this.setDependentProductIds(source.getDependentProductIds());
this.setCreated(source.getCreated() != null ? (Date) source.getCreated().clone() : null);
this.setUpdated(source.getUpdated() != null ? (Date) source.getUpdated().clone() : null);
this.setLocked(source.isLocked());
}
/**
* Copies several properties from the given product on to this product instance. Properties that
* are not copied over include any identifiying fields (UUID, ID), the creation date and locking
* states. Values on the source product which are null will be ignored.
*
* @param source
* The source product instance from which to pull product information
*
* @return
* this product instance
*/
public Product merge(Product source) {
if (source.getName() != null) {
this.setName(source.getName());
}
if (source.getMultiplier() != null) {
this.setMultiplier(source.getMultiplier());
}
// Copy attributes
this.setAttributes(source.getAttributes());
// Copy content
if (!Util.collectionsAreEqual(source.getProductContent(), this.getProductContent())) {
List<ProductContent> content = new LinkedList<ProductContent>();
for (ProductContent src : source.getProductContent()) {
ProductContent dest = new ProductContent(this, src.getContent(), src.isEnabled());
dest.setCreated(src.getCreated() != null ? (Date) src.getCreated().clone() : null);
dest.setUpdated(src.getUpdated() != null ? (Date) src.getUpdated().clone() : null);
content.add(dest);
}
this.setProductContent(content);
}
// Copy dependent product IDs
this.setDependentProductIds(source.getDependentProductIds());
this.setUpdated(source.getUpdated() != null ? (Date) source.getUpdated().clone() : null);
return this;
}
@Override
public Object clone() {
Product copy;
try {
copy = (Product) 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 attributes
copy.attributes = new HashMap<String, String>();
copy.attributes.putAll(this.attributes);
// Copy content
copy.productContent = new LinkedList<ProductContent>();
for (ProductContent src : this.getProductContent()) {
ProductContent dest = new ProductContent(copy, src.getContent(), src.isEnabled());
dest.setCreated(src.getCreated() != null ? (Date) src.getCreated().clone() : null);
dest.setUpdated(src.getUpdated() != null ? (Date) src.getUpdated().clone() : null);
copy.productContent.add(dest);
}
// Copy dependent product IDs
copy.dependentProductIds = new HashSet<String>();
copy.dependentProductIds.addAll(this.dependentProductIds);
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 ProductData toDTO() {
return new ProductData(this);
}
/**
* Retrieves this product's object/database UUID. While the product ID may exist multiple times
* in the database (if in use by multiple owners), this UUID uniquely identifies a
* product instance.
*
* @return
* this product's database UUID.
*/
public String getUuid() {
return uuid;
}
/**
* Sets this product's object/database ID. Note that this ID is used to uniquely identify this
* particular object and has no baring on the Red Hat product ID.
*
* @param uuid
* The object ID to assign to this product.
*/
public void setUuid(String uuid) {
this.uuid = uuid;
}
/**
* Retrieves this product's ID. Assigned by the content provider, and may exist in
* multiple owners, thus may not be unique in itself.
*
* @return
* this product's ID.
*/
public String getId() {
return this.id;
}
/**
* Sets the product ID for this product. The product ID is the Red Hat product ID and should not
* be confused with the object ID.
*
* @param productId
* The new product ID for this product.
*/
public void setId(String productId) {
this.id = productId;
}
/**
* @return the product name
*/
public String getName() {
return name;
}
/**
* sets the product name.
*
* @param name name of the product
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the number of entitlements to create from a single subscription
*/
public Long getMultiplier() {
return multiplier;
}
/**
* @param multiplier the multiplier to set
*/
public void setMultiplier(Long multiplier) {
if (multiplier == null) {
this.multiplier = 1L;
}
else {
this.multiplier = Math.max(1L, multiplier);
}
}
/**
* Retrieves the attributes for this product. If this product does not have any attributes,
* this method returns an empty map.
*
* @return
* a map containing the attributes for this product
*/
public Map<String, String> getAttributes() {
return new MapView(this.attributes);
}
/**
* Retrieves the value associated with the given attribute. If the attribute is not set, this
* method returns null.
*
* @param key
* The key (name) of the attribute to lookup
*
* @throws IllegalArgumentException
* if key is null
*
* @return
* the value set for the given attribute, or null if the attribute is not set
*/
@XmlTransient
public String getAttributeValue(String key) {
if (key == null) {
throw new IllegalArgumentException("key is null");
}
return this.attributes.get(key);
}
/**
* Checks if the given attribute has been defined on this product.
*
* @param key
* The key (name) of the attribute to lookup
*
* @throws IllegalArgumentException
* if key is null
*
* @return
* true if the attribute is defined for this product; false otherwise
*/
@XmlTransient
public boolean hasAttribute(String key) {
if (key == null) {
throw new IllegalArgumentException("key is null");
}
return this.attributes.containsKey(key);
}
/**
* Sets the specified attribute for this product. If the attribute has already been set for
* this product, the existing value will be overwritten. If the given attribute value is null
* or empty, the attribute will be removed.
*
* @param key
* The name or key of the attribute to set
*
* @param value
* The value to assign to the attribute, or null to remove the attribute
*
* @throws IllegalArgumentException
* if key is null
*
* @return
* a reference to this product
*/
public Product setAttribute(String key, String value) {
if (key == null) {
throw new IllegalArgumentException("key is null");
}
// Impl note:
// We can't standardize the value at all here; some attributes allow null, some expect
// empty strings, and others have their own sential values. Unless we make a concerted
// effort to fix all of these inconsistencies with a massive database update, we can't
// perform any input sanitation/massaging.
this.attributes.put(key, value);
return this;
}
/**
* Removes the attribute with the given attribute key from this product.
*
* @param key
* The name/key of the attribute to remove
*
* @throws IllegalArgumentException
* if key is null
*
* @return
* true if the attribute was removed successfully; false otherwise
*/
public boolean removeAttribute(String key) {
if (key == null) {
throw new IllegalArgumentException("key is null");
}
boolean present = this.attributes.containsKey(key);
this.attributes.remove(key);
return present;
}
/**
* Clears all attributes currently set for this product.
*
* @return
* a reference to this product
*/
public Product clearAttributes() {
this.attributes.clear();
return this;
}
/**
* Sets the attributes for this product.
*
* @param attributes
* A map of attribute key, value pairs to assign to this product, or null to clear the
* attributes
*
* @return
* a reference to this product
*/
public Product setAttributes(Map<String, String> attributes) {
this.attributes.clear();
if (attributes != null) {
this.attributes.putAll(attributes);
}
return this;
}
@XmlTransient
public List<String> getSkuEnabledContentIds() {
List<String> skus = new LinkedList<String>();
String attrib = this.getAttributeValue(Attributes.CONTENT_OVERRIDE_ENABLED);
if (attrib != null && !attrib.isEmpty()) {
StringTokenizer tokenizer = new StringTokenizer(attrib, ",");
while (tokenizer.hasMoreElements()) {
skus.add((String) tokenizer.nextElement());
}
}
return skus;
}
@XmlTransient
public List<String> getSkuDisabledContentIds() {
List<String> skus = new LinkedList<String>();
String attrib = this.getAttributeValue(Attributes.CONTENT_OVERRIDE_DISABLED);
if (attrib != null && !attrib.isEmpty()) {
StringTokenizer tokenizer = new StringTokenizer(attrib, ",");
while (tokenizer.hasMoreElements()) {
skus.add((String) tokenizer.nextElement());
}
}
return skus;
}
/**
* Retrieves the content of the product represented by this product. If this product does not
* have any associated content, this method returns an empty collection.
*
* @return
* the product content associated with this product
*/
public Collection<ProductContent> getProductContent() {
return new ListView(this.productContent);
}
/**
* Retrieves the product content for the specified content ID. If no such content has been
* assocaited with this product, this method returns null.
*
* @param contentId
* The ID of the content to retrieve
*
* @throws IllegalArgumentException
* if contentId is null
*
* @return
* the content associated with this product using the given ID, or null if such content does
* not exist
*/
public ProductContent getProductContent(String contentId) {
if (contentId == null) {
throw new IllegalArgumentException("contentId is null");
}
for (ProductContent pcd : this.productContent) {
if (pcd.getContent() != null && contentId.equals(pcd.getContent().getId())) {
return pcd;
}
}
return null;
}
/**
* Checks if any content with the given content ID has been associated with this product.
*
* @param contentId
* The ID of the content to check
*
* @throws IllegalArgumentException
* if contentId is null
*
* @return
* true if any content with the given content ID has been associated with this product; false
* otherwise
*/
public boolean hasContent(String contentId) {
if (contentId == null) {
throw new IllegalArgumentException("contentId is null");
}
return this.getProductContent(contentId) != null;
}
/**
* Adds the given content to this product. If a matching content has already been added to
* this product, it will be overwritten by the specified content.
*
* @param productContent
* The product content to add to this product
*
* @throws IllegalArgumentException
* if content is null or incomplete
*
* @return
* true if adding the content resulted in a change to this product; false otherwise
*/
public boolean addProductContent(ProductContent productContent) {
if (productContent == null) {
throw new IllegalArgumentException("productContent is null");
}
if (productContent.getContent() == null || productContent.getContent().getId() == null) {
throw new IllegalArgumentException("content is incomplete");
}
boolean changed = false;
boolean matched = false;
Collection<ProductContent> remove = new LinkedList<ProductContent>();
// We're operating under the assumption that we won't be doing janky things like
// adding product content, then changing it. It's too bad this isn't all immutable...
for (ProductContent pcd : this.productContent) {
Content cd = pcd.getContent();
if (cd != null && cd.getId() != null && cd.getId().equals(productContent.getContent().getId())) {
matched = true;
if (pcd.isEnabled() != productContent.isEnabled() ||
!cd.equals(productContent.getContent())) {
remove.add(pcd);
}
}
}
if (!matched || remove.size() > 0) {
productContent.setProduct(this);
this.productContent.removeAll(remove);
changed = this.productContent.add(productContent);
}
return changed;
}
/**
* Adds the given content to this product. If a matching content has already been added to
* this product, it will be overwritten by the specified content.
*
* @param content
* The product content to add to this product
*
* @param enabled
* Whether or not the content should be enabled for this product
*
* @throws IllegalArgumentException
* if content is null
*
* @return
* true if adding the content resulted in a change to this product; false otherwise
*/
public boolean addContent(Content content, boolean enabled) {
if (content == null) {
throw new IllegalArgumentException("content is null");
}
return this.addProductContent(new ProductContent(this, content, enabled));
}
/**
* Removes the content with the given content ID from this product.
*
* @param contentId
* The ID of the content to remove
*
* @throws IllegalArgumentException
* if contentId is null
*
* @return
* true if the content was removed successfully; false otherwise
*/
public boolean removeContent(String contentId) {
if (contentId == null) {
throw new IllegalArgumentException("contentId is null");
}
Collection<ProductContent> remove = new LinkedList<ProductContent>();
for (ProductContent pcd : this.productContent) {
Content cd = pcd.getContent();
if (cd != null && contentId.equals(cd.getId())) {
remove.add(pcd);
}
}
return this.productContent.removeAll(remove);
}
/**
* Removes the content represented by the given content entity from this product. Any content
* with the same ID as the ID of the given content entity will be removed.
*
* @param content
* The content entity representing the content to remove from this product
*
* @throws IllegalArgumentException
* if content is null or incomplete
*
* @return
* true if the content was removed successfully; false otherwise
*/
public boolean removeContent(Content content) {
if (content == null) {
throw new IllegalArgumentException("content is null");
}
if (content.getId() == null) {
throw new IllegalArgumentException("content is incomplete");
}
return this.removeContent(content.getId());
}
/**
* Removes the content represented by the given content entity from this product. Any content
* with the same ID as the ID of the given content entity will be removed.
*
* @param content
* The product content entity representing the content to remove from this product
*
* @throws IllegalArgumentException
* if content is null or incomplete
*
* @return
* true if the content was removed successfully; false otherwise
*/
public boolean removeProductContent(ProductContent content) {
if (content == null) {
throw new IllegalArgumentException("content is null");
}
if (content.getContent() == null || content.getContent().getId() == null) {
throw new IllegalArgumentException("content is incomplete");
}
return this.removeContent(content.getContent().getId());
}
/**
* Clears all product content currently associated with this product.
*
* @return
* a reference to this product
*/
public Product clearProductContent() {
this.productContent.clear();
return this;
}
/**
* Sets the content of the product represented by this product.
*
* @param content
* A collection of product content to attach to this product, or null to clear the content
*
* @return
* a reference to this product
*/
public Product setProductContent(Collection<ProductContent> content) {
this.productContent.clear();
if (content != null) {
for (ProductContent pcd : content) {
this.addProductContent(pcd);
}
}
return this;
}
/**
* Returns true if this product has a content set which modifies the given
* product:
*
* @param productId
* @return true if this product modifies the given product ID
*/
public boolean modifies(String productId) {
for (ProductContent pc : this.productContent) {
if (pc.getContent().getModifiedProductIds().contains(productId)) {
return true;
}
}
return false;
}
/**
* Retrieves the dependent product IDs for this product. If the dependent product IDs have not
* yet been defined, this method returns an empty collection.
*
* @return
* the dependent product IDs of this product
*/
public Collection<String> getDependentProductIds() {
return new SetView(this.dependentProductIds);
}
/**
* Adds the ID of the specified product as a dependent product of this product. If the product
* is already a dependent product, it will not be added again.
*
* @param productId
* The ID of the product to add as a dependent product
*
* @throws IllegalArgumentException
* if productId is null
*
* @return
* true if the dependent product was added successfully; false otherwise
*/
public boolean addDependentProductId(String productId) {
if (productId == null) {
throw new IllegalArgumentException("productId is null");
}
return this.dependentProductIds.add(productId);
}
/**
* Removes the specified product as a dependent product of this product. If the product is not
* dependent on this product, this method does nothing.
*
* @param productId
* The ID of the product to add as a dependent product
*
* @throws IllegalArgumentException
* if productId is null
*
* @return
* true if the dependent product was removed successfully; false otherwise
*/
public boolean removeDependentProductId(String productId) {
if (productId == null) {
throw new IllegalArgumentException("productId is null");
}
return this.dependentProductIds.remove(productId);
}
/**
* Clears all dependent product IDs currently set for this product.
*
* @return
* a reference to this product
*/
public Product clearDependentProductIds() {
this.dependentProductIds.clear();
return this;
}
/**
* Sets the dependent product IDs of this product.
*
* @param dependentProductIds
* A collection of dependent product IDs to attach to this product, or null to clear the
* dependent products
*
* @return
* a reference to this product
*/
public Product setDependentProductIds(Collection<String> dependentProductIds) {
this.dependentProductIds.clear();
if (dependentProductIds != null) {
for (String pid : dependentProductIds) {
this.addDependentProductId(pid);
}
}
return this;
}
public String getHref() {
return this.uuid != null ? String.format("/products/%s", this.uuid) : null;
}
@Override
public String toString() {
return String.format("Product [uuid: %s, id: %s, name: %s]", this.uuid, this.id, this.name);
}
@XmlTransient
@JsonIgnore
public Product setLocked(boolean locked) {
this.locked = locked;
return this;
}
@XmlTransient
public boolean isLocked() {
return this.locked;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
boolean equals = false;
if (obj instanceof Product) {
Product that = (Product) obj;
// TODO:
// Maybe it would be better to check the UUID field and only check the following if
// both products have null UUIDs? By not doing this check, we run the risk of two
// different products being considered equal if they happen to have the same values at
// the time they're checked, or two products not being considered equal if they
// represent the same product in different states.
equals = new EqualsBuilder()
.append(this.id, that.id)
.append(this.name, that.name)
.append(this.multiplier, that.multiplier)
.append(this.locked, that.locked)
.append(this.attributes, that.attributes)
.isEquals();
// Check our collections.
// Impl note: We can't use .equals here on the collections, as Hibernate's special
// collections explicitly state that they break the contract on .equals. As such, we
// have to step through each collection and do a manual comparison. Ugh.
equals = equals &&
Util.collectionsAreEqual(this.dependentProductIds, that.dependentProductIds) &&
Util.collectionsAreEqual(this.productContent, that.productContent);
}
return equals;
}
@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.name)
.append(this.multiplier)
.append(this.locked)
.append(this.attributes);
// Impl note:
// Stepping through the collections here is as painful as it looks, but Hibernate, once
// again, doesn't implement .hashCode reliably on the proxy collections. So, we have to
// manually step through these and add the elements to ensure the hash code is
// generated properly.
int accumulator = 0;
if (!this.dependentProductIds.isEmpty()) {
accumulator = 0;
for (String pid : this.dependentProductIds) {
accumulator += (pid != null ? pid.hashCode() : 0);
}
builder.append(accumulator);
}
if (!this.productContent.isEmpty()) {
accumulator = 0;
for (ProductContent pc : this.productContent) {
accumulator += (pc != null ? pc.getEntityVersion() : 0);
}
builder.append(accumulator);
}
return builder.toHashCode();
}
/**
* Determines whether or not this entity would be changed if the given DTO were applied to this
* object.
*
* @param dto
* The product DTO to check for changes
*
* @throws IllegalArgumentException
* if dto is null
*
* @return
* true if this product would be changed by the given DTO; false otherwise
*/
public boolean isChangedBy(ProductData dto) {
if (dto == null) {
throw new IllegalArgumentException("dto is null");
}
// Check simple properties first
if (dto.getId() != null && !dto.getId().equals(this.id)) {
return true;
}
if (dto.getName() != null && !dto.getName().equals(this.name)) {
return true;
}
if (dto.getMultiplier() != null && !dto.getMultiplier().equals(this.multiplier)) {
return true;
}
if (dto.isLocked() != null && !dto.isLocked().equals(this.locked)) {
return true;
}
Collection<String> dependentProductIds = dto.getDependentProductIds();
if (dependentProductIds != null &&
!Util.collectionsAreEqual(this.dependentProductIds, dependentProductIds)) {
return true;
}
// Impl note:
// Depending on how strict we are regarding case-sensitivity and value-representation,
// this may get us in to trouble. We may need to iterate through the attributes, performing
// case-insensitive key/value comparison and similiarities (i.e. management_enabled: 1 is
// functionally identical to Management_Enabled: true, but it will be detected as a change
// in attributes.
Map<String, String> attributes = dto.getAttributes();
if (attributes != null && !this.attributes.equals(attributes)) {
return true;
}
Collection<ProductContentData> productContent = dto.getProductContent();
if (productContent != null) {
Comparator comparator = new Comparator<Object>() {
public int compare(Object lhs, Object rhs) {
return ((ProductContent) lhs).isChangedBy((ProductContentData) rhs) ? 1 : 0;
}
};
if (!Util.collectionsAreEqual(
(Collection) this.productContent, (Collection) productContent, comparator)) {
return true;
}
}
return false;
}
@PrePersist
@PreUpdate
public void updateEntityVersion() {
this.entityVersion = this.getEntityVersion();
}
}