/* * Copyright (c) 2009-2012 Lockheed Martin Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.eurekastreams.server.domain; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.persistence.Basic; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.Lob; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import javax.persistence.PostPersist; import javax.persistence.PostUpdate; import javax.persistence.Temporal; import javax.persistence.TemporalType; import javax.persistence.Transient; import javax.persistence.UniqueConstraint; import org.apache.lucene.analysis.WhitespaceAnalyzer; import org.eurekastreams.commons.model.DomainEntity; import org.eurekastreams.commons.search.analysis.HtmlStemmerAnalyzer; import org.eurekastreams.commons.search.analysis.TextStemmerAnalyzer; import org.eurekastreams.server.domain.stream.StreamScope; import org.eurekastreams.server.search.bridge.BackgroundItemListStringBridge; import org.eurekastreams.server.search.bridge.DomainGroupPeopleIdClassBridge; import org.eurekastreams.server.search.bridge.IsGroupVisibleInSearchClassBridge; import org.hibernate.search.annotations.Analyzer; import org.hibernate.search.annotations.ClassBridge; import org.hibernate.search.annotations.ClassBridges; import org.hibernate.search.annotations.DateBridge; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.FieldBridge; import org.hibernate.search.annotations.Index; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.annotations.Resolution; import org.hibernate.search.annotations.Store; import org.hibernate.validator.Length; import org.hibernate.validator.Pattern; import org.hibernate.validator.Size; /** * Represents a group, which holds people. */ @Entity @Indexed @ClassBridges(value = { @ClassBridge(name = "followerAndCoordinatorIds", index = Index.TOKENIZED, store = Store.NO, // whitespace analyzer and custom class bridge to use JPA to get the ids rather than load extra objects analyzer = @Analyzer(impl = WhitespaceAnalyzer.class), impl = DomainGroupPeopleIdClassBridge.class), @ClassBridge(name = "isVisibleInSearch", index = Index.UN_TOKENIZED, store = Store.NO, // \n impl = IsGroupVisibleInSearchClassBridge.class) }) public class DomainGroup extends DomainEntity implements AvatarEntity, Followable, DomainGroupEntity, CompositeEntity, Identifiable { /** * Serial version uid. */ private static final long serialVersionUID = 6833923705995476358L; /** Used for validation. */ @Transient public static final int MAX_NAME_LENGTH = 50; /** Used for validation. */ @Transient public static final int MAX_SHORT_NAME_LENGTH = 20; /** Used for validation. */ @Transient public static final int MAX_DESCRIPTION_LENGTH = 250; // TODO Messages should be moved to the group model view. /** Used for validation. */ @Transient public static final String NAME_LENGTH_MESSAGE = "Group Name supports up to " + MAX_NAME_LENGTH + " characters."; /** Used for validation. */ @Transient public static final String SHORT_NAME_LENGTH_MESSAGE = "Group Web Address supports up to " + MAX_SHORT_NAME_LENGTH + " characters."; /** Used for validation. */ @Transient public static final String NAME_REQUIRED = "Group Name is required."; /** Used for validation. */ @Transient public static final String DESCRIPTION_REQUIRED = "Group description is required."; /** Used for validation. */ @Transient public static final String SHORTNAME_REQUIRED = "Group Web Address is required."; /** Used for validation. */ @Transient public static final String MIN_COORDINATORS_MESSAGE = "Groups must have at least one coordinator."; /** Used for validation. */ @Transient public static final String DESCRIPTION_LENGTH_MESSAGE = "Description supports up to " + MAX_DESCRIPTION_LENGTH + " characters."; /** Used for validation. */ @Transient public static final String ALPHA_NUMERIC_PATTERN = "[A-Za-z0-9]*"; /** Used for validation. */ @Transient public static final String SHORT_NAME_CHARACTERS = "A short name can only contain " + "alphanumeric characters and no spaces."; /** Pattern for validating legal group names. */ @Transient public static final String GROUP_NAME_PATTERN = "^[ a-zA-Z0-9~!@#$%^&*()\\-_=+;:'\",./?]+$"; /** Message for failure to validate group names. */ @Transient public static final String GROUP_NAME_MESSAGE = "Name has invalid characters."; /** * The name of the group. */ @Basic(optional = false) @Length(min = 1, max = MAX_NAME_LENGTH, message = NAME_LENGTH_MESSAGE) @Field(name = "name", index = Index.TOKENIZED, // use text stemmer for index and search analyzer = @Analyzer(impl = TextStemmerAnalyzer.class), store = Store.NO) private String name; /** * The short version of group name. */ @Column(nullable = false, unique = true) @Length(min = 1, max = MAX_SHORT_NAME_LENGTH, message = SHORT_NAME_LENGTH_MESSAGE) @Pattern(regex = ALPHA_NUMERIC_PATTERN, message = SHORT_NAME_CHARACTERS) @Field(name = "shortName", index = Index.UN_TOKENIZED, store = Store.NO) private String shortName; /** * The overview of the group. */ @Basic @Lob @Field(name = "overview", index = Index.TOKENIZED, store = Store.NO, // html-stemmer analyzer will be used for indexing and, text-stemmer for searching analyzer = @Analyzer(impl = HtmlStemmerAnalyzer.class)) private String overview; /** * The description statement of the group. */ @Basic @Length(min = 1, max = MAX_DESCRIPTION_LENGTH, message = DESCRIPTION_LENGTH_MESSAGE) @Field(name = "description", index = Index.TOKENIZED, store = Store.NO, // text-stemmer analyzer will be used for indexing and, text-stemmer for searching analyzer = @Analyzer(impl = TextStemmerAnalyzer.class)) private String description; /** * The date the group was added into the system, defaults to the current time, indexed into search engine. Note, for * the date to be sortable, it needs to be either Index.UN_TOKENIZED or Index.NO_NORMS. */ @Column(nullable = false) @Field(name = "dateAdded", index = Index.UN_TOKENIZED, store = Store.NO) @Temporal(TemporalType.TIMESTAMP) @DateBridge(resolution = Resolution.SECOND) private Date dateAdded = new Date(); /** * List of coordinators for this group. */ @Size(min = 1, message = MIN_COORDINATORS_MESSAGE) @ManyToMany(fetch = FetchType.EAGER, cascade = { CascadeType.PERSIST }) @JoinTable(name = "Group_Coordinators") private Set<Person> coordinators; /** * Person who created the group. */ @ManyToOne(fetch = FetchType.EAGER, cascade = { CascadeType.PERSIST }) @JoinColumn(name = "createdById") private Person createdBy = new Person(); /** * The skills that are contained in this background. */ @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.PERSIST }) @JoinTable(name = "Group_Capability", // join columns joinColumns = { @JoinColumn(table = "DomainGroup", name = "domainGroupId") }, // inverse join columns inverseJoinColumns = { @JoinColumn(table = "BackgroundItem", name = "capabilityId") }, // unique constraints uniqueConstraints = { @UniqueConstraint(columnNames = { "domainGroupId", "capabilityId" }) }) @Field(name = "capabilities", bridge = @FieldBridge(impl = BackgroundItemListStringBridge.class), // line break index = Index.TOKENIZED, store = Store.NO, analyzer = @Analyzer(impl = TextStemmerAnalyzer.class)) private List<BackgroundItem> capabilities; /** * Whether this is a public group (true) or a private group (false). */ @Basic @Field(name = "isPublic", index = Index.UN_TOKENIZED, store = Store.NO) private boolean publicGroup; /** * Whether the entity allows comments on their post. */ @Basic(optional = false) @Field(name = "isCommentable", index = Index.UN_TOKENIZED, store = Store.NO) private boolean commentable = true; /** * Whether the entity allows people to post to their wall. */ @Basic(optional = false) @Field(name = "isStreamPostable", index = Index.UN_TOKENIZED, store = Store.NO) private boolean streamPostable = true; /** * The url of the group. */ @Basic(optional = true) @Pattern(regex = URL_REGEX_PATTERN, message = WEBSITE_MESSAGE) private String url; /** * */ @Basic private Integer avatarCropX; /** * */ @Basic private Integer avatarCropY; /** * */ @Basic private Integer avatarCropSize; /** * avatar id image for this user. */ @Basic private String avatarId; /** * Count of people following this group. */ @Basic(optional = false) @Field(name = "followersCount", index = Index.UN_TOKENIZED, store = Store.NO) private int followersCount; /** * The number of updates for this group. */ @Basic(optional = false) @Field(name = "updatesCount", index = Index.UN_TOKENIZED, store = Store.NO) private int updatesCount = 0; /** * Only used for query reference, don't load this. */ @SuppressWarnings("unused") @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "GroupFollower", // join columns joinColumns = { @JoinColumn(table = "DomainGroup", name = "followingId") }, // inverse joincolumns inverseJoinColumns = { @JoinColumn(table = "Person", name = "followerId") }) private List<Person> followers; /** * banner id for this org. */ @Basic private String bannerId; /** * Transient field used only for displaying a banner on profile pages. This is needed so that a common strategy can * be used across groups, orgs, and people to display banners. When profiles support DTO's, this can be moved there. */ @Transient private Long bannerEntityId; /** * Stream scope representing this group. */ @OneToOne(fetch = FetchType.EAGER, cascade = { CascadeType.PERSIST, CascadeType.REMOVE }) @JoinColumn(name = "streamScopeId") private StreamScope streamScope; /** * The approval status of the group. */ @Basic @Field(name = "isPending", index = Index.UN_TOKENIZED, store = Store.NO) private boolean isPending; /** ID of the activity stuck at the top of the stream. Null if none. */ @Basic(optional = true) private Long stickyActivityId; /** * Retrieve the name of the DomainEntity. This is to allow for the super class to identify the table within * hibernate. * * @return The name of the domain entity. */ public static String getDomainEntityName() { return "DomainGroup"; } /** * Default constructor. */ public DomainGroup() { // no-op } /** * set the id - useful for unit testing. * * @param newId * the new id */ @Override protected void setId(final long newId) { super.setId(newId); } /** * Override equality to be based on the group's id. * * @param rhs * target object * @return true if equal, false otherwise. */ @Override public boolean equals(final Object rhs) { return (rhs instanceof DomainGroup && getId() == ((DomainGroup) rhs).getId()); } /** * HashCode override. * * @see java.lang.Object#hashCode() * @return hashcode for object. */ @Override public int hashCode() { // NOTE: unable to use HashCodeBuilder here due to GWT limitation. int hashCode = 0; hashCode ^= (new Long(getId())).hashCode(); hashCode ^= shortName.hashCode(); return hashCode; } /** * People who are requesting membership to the group. Only used if the group is private. Field is private with no * getters/setters since it is used only for table/key creation. */ @SuppressWarnings("unused") @ManyToMany(fetch = FetchType.LAZY, cascade = { CascadeType.ALL }) @JoinTable(name = "GroupMembershipRequests", // join columns joinColumns = { @JoinColumn(table = "DomainGroup", name = "groupId") }, // inverse join columns inverseJoinColumns = { @JoinColumn(table = "Person", name = "personId") }) private Set<Person> membershipRequests; /** * Constructor. This should have every non-null parameter included. * * @param inName * - Full name of group. * @param inShortName * Short name of group. * @param inCreatedBy * The Person that created the group. */ public DomainGroup(final String inName, final String inShortName, final Person inCreatedBy) { name = inName; setShortName(inShortName); createdBy = inCreatedBy; } /** * Add coordinator to group. * * @param person * The Person to add. */ public void addCoordinator(final Person person) { if (coordinators == null) { coordinators = new HashSet<Person>(); } coordinators.add(person); } /** * Getter for list of coordinators. * * @return list of coordinators. */ @Override public Set<Person> getCoordinators() { return coordinators; } /** * Setter for list of coordinators. * * @param inCoordinators * list of coordinators. */ public void setCoordinators(final Set<Person> inCoordinators) { coordinators = inCoordinators; } /** * @return the capabilities */ @Override public List<BackgroundItem> getCapabilities() { return (capabilities == null) ? new ArrayList<BackgroundItem>(0) : capabilities; } /** * @param inCapabilities * the capabilities to set */ @Override public void setCapabilities(final List<BackgroundItem> inCapabilities) { capabilities = inCapabilities; } /** * @return the group's name */ @Override public String getName() { return name; } /** * Setter for group name. * * @param inName * new name */ @Override public void setName(final String inName) { name = (null == inName) ? "" : inName; } /** * Getter for group short name. * * @return the shortName */ @Override public String getShortName() { return shortName; } /** * Setter for group short name. * * @param inShortName * the shortName to set. */ public void setShortName(final String inShortName) { shortName = (null == inShortName) ? "" : inShortName.toLowerCase(); } /** * Getter. * * @return the overview */ @Override public String getOverview() { return overview; } /** * Setter. * * @param inOverview * the overview to set */ @Override public void setOverview(final String inOverview) { overview = inOverview; } /** * @return the description */ @Override public String getDescription() { return description; } /** * @param inDescription * the description to set */ public void setDescription(final String inDescription) { description = inDescription; } /** * check to see if the specified account id is a coordinator for this group. * * @param account * to check. * @return if they're a coordinator. */ @Override public boolean isCoordinator(final String account) { for (Person p : coordinators) { if (p.getAccountId().equals(account)) { return true; } } return false; } /** * @return the publicGroup */ @Override public boolean isPublicGroup() { return publicGroup; } /** * @param inPublicGroup * the publicGroup to set */ public void setPublicGroup(final boolean inPublicGroup) { publicGroup = inPublicGroup; } /** * Get avatar x coord. * * @return avatar x coord. */ @Override public Integer getAvatarCropX() { return avatarCropX; } /** * Set avatar x coord. * * @param value * x coord. */ @Override public void setAvatarCropX(final Integer value) { avatarCropX = value; } /** * Get avatar y coord. * * @return avatar y coord. */ @Override public Integer getAvatarCropY() { return avatarCropY; } /** * Set avatar y coord. * * @param value * y coord. */ @Override public void setAvatarCropY(final Integer value) { avatarCropY = value; } /** * Get avatar crop size. * * @return avatar crop size. */ @Override public Integer getAvatarCropSize() { return avatarCropSize; } /** * Set avatar crop size. * * @param value * crop size. */ @Override public void setAvatarCropSize(final Integer value) { avatarCropSize = value; } /** * @return the avatar Id */ @Override public String getAvatarId() { return avatarId; } /** * @param inAvatarId * the avatar to set */ @Override public void setAvatarId(final String inAvatarId) { avatarId = inAvatarId; } /** * @return the followersCount */ @Override public int getFollowersCount() { return followersCount; } /** * @param inFollowersCount * the followersCount to set */ public void setFollowersCount(final int inFollowersCount) { followersCount = inFollowersCount; } /** * @return the group's banner id */ @Override public String getBannerId() { return bannerId; } /** * @param inBannerId * the banner to set */ @Override public void setBannerId(final String inBannerId) { bannerId = inBannerId; } /** * Get the number of updates for this group. * * @return the updatesCount */ public int getUpdatesCount() { return updatesCount; } /** * Set the number of updates for this group. * * @param inUpdatesCount * the updatesCount to set */ protected void setUpdatesCount(final int inUpdatesCount) { updatesCount = inUpdatesCount; } /** * Set the date the group was added to the system. * * @param inDateAdded * the dateAdded to set */ protected void setDateAdded(final Date inDateAdded) { dateAdded = inDateAdded; } /** * Get the date the group was added to the system. * * @return the dateAdded */ public Date getDateAdded() { return dateAdded; } /** * @return the status */ public boolean isPending() { return isPending; } /** * Sets if the group is pending. Note: Name is awkward, but follows the bean spec. (setIsPending would go with * getIsPending; setIsPending is NOT a match for isPending and thus the field doesn't serialize) * * @param inIsPending * the status to set */ public void setPending(final boolean inIsPending) { isPending = inIsPending; } /** * @param inCreatedBy * set the person who created the group. */ public void setCreatedBy(final Person inCreatedBy) { createdBy = inCreatedBy; } /** * @return person who created the group. */ public Person getCreatedBy() { return createdBy; } /** * @return if the profile is set to allow comments. */ public boolean isCommentable() { return commentable; } /** * @param inCommentable * if the profile is set to allow comments. */ public void setCommentable(final boolean inCommentable) { commentable = inCommentable; } /** * @return if the profile is set to all wall comments. */ public boolean isStreamPostable() { return streamPostable; } /** * @param inStreamPostable * set the wall comment property. */ public void setStreamPostable(final boolean inStreamPostable) { streamPostable = inStreamPostable; } // ---------------------------------------------------- // ------------------ CACHE UPDATING ------------------ /** * Call-back after a Person entity has been updated. This tells the static cacheUpdater if set. */ @SuppressWarnings("unused") @PostUpdate private void onPostUpdate() { if (entityCacheUpdater != null) { entityCacheUpdater.onPostUpdate(this); } } /** * Call-back after the entity has been persisted. This tells the static cacheUpdater if set. */ @SuppressWarnings("unused") @PostPersist private void onPostPersist() { if (entityCacheUpdater != null) { entityCacheUpdater.onPostPersist(this); } } /** * The entity cache updater. */ private static transient EntityCacheUpdater<DomainGroup> entityCacheUpdater; /** * Setter for the static PersonUpdater. * * @param inEntityCacheUpdater * the PersonUpdater to set */ public static void setEntityCacheUpdater(final EntityCacheUpdater<DomainGroup> inEntityCacheUpdater) { entityCacheUpdater = inEntityCacheUpdater; } // ---------------- END CACHE UPDATING ---------------- // ---------------------------------------------------- /** * @return the streamScope */ public StreamScope getStreamScope() { return streamScope; } /** * @param inStreamScope * the streamScope to set */ public void setStreamScope(final StreamScope inStreamScope) { streamScope = inStreamScope; } /** * Getter for the domain short name as implementation for Followable. * * @return - UniqueId of the Group - shortname. */ @Override public String getUniqueId() { return getShortName(); } /** * {@inheritDoc}. */ @Override public Long getBannerEntityId() { return bannerEntityId; } /** * {@inheritDoc}. */ @Override public void setBannerEntityId(final Long inBannerEntityId) { bannerEntityId = inBannerEntityId; } /** * {@inheritDoc} */ @Override public EntityType getEntityType() { return EntityType.GROUP; } /** * {@inheritDoc} */ @Override public String getDisplayName() { return name; } /** * {@inheritDoc} */ @Override public long getEntityId() { return getId(); } /** * @return the url */ public String getUrl() { return url; } /** * @param inUrl * the url to set */ public void setUrl(final String inUrl) { url = inUrl; } /** * @return the stuckActivityId */ public Long getStickyActivityId() { return stickyActivityId; } /** * @param inStickyActivityId * the stuckActivityId to set */ public void setStickyActivityId(final Long inStickyActivityId) { stickyActivityId = inStickyActivityId; } }