/* * Copyright (c) 2009-2010 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.List; import java.util.Set; import java.util.TreeSet; 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.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.commons.search.bridge.StandardAnalyzerSortFieldBridge; import org.eurekastreams.server.domain.stream.StreamScope; import org.eurekastreams.server.search.bridge.BackgroundItemListStringBridge; import org.eurekastreams.server.search.bridge.IsRootOrganizationClassBridge; import org.eurekastreams.server.search.bridge.OrgIdHierarchyFieldBridge; import org.eurekastreams.server.search.bridge.OrganizationToShortNameFieldBridge; import org.eurekastreams.server.search.modelview.OrganizationModelView; import org.hibernate.annotations.Formula; import org.hibernate.annotations.LazyCollection; import org.hibernate.annotations.LazyCollectionOption; import org.hibernate.annotations.Where; import org.hibernate.search.annotations.Analyzer; import org.hibernate.search.annotations.ClassBridge; import org.hibernate.search.annotations.Field; import org.hibernate.search.annotations.FieldBridge; import org.hibernate.search.annotations.Fields; import org.hibernate.search.annotations.Index; import org.hibernate.search.annotations.Indexed; import org.hibernate.search.annotations.Store; import org.hibernate.validator.Length; import org.hibernate.validator.Pattern; import org.hibernate.validator.Size; /** * Represents an organization, which holds people. */ @SuppressWarnings("serial") @Entity @Indexed @ClassBridge(name = "isRootOrganization", index = Index.UN_TOKENIZED, store = Store.NO, // (line length) impl = IsRootOrganizationClassBridge.class) public class Organization extends DomainEntity implements OrganizationChild, AvatarEntity, CompositeEntity, Bannerable { /** Used for validation. */ public static final int MAX_NAME_LENGTH = 50; /** Used for validation. */ public static final int MAX_DESCRIPTION_LENGTH = 250; /** Used for validation. */ public static final int MAX_SHORT_NAME_LENGTH = 20; // TODO Messages should be moved into the ui model. /** Used for validation. */ public static final String NAME_LENGTH_MESSAGE = "Organization Name must supports up to " + MAX_NAME_LENGTH + " characters."; /** Used for validation. */ public static final String SHORT_NAME_LENGTH_MESSAGE = "Organization Web Address supports up to " + MAX_SHORT_NAME_LENGTH + " characters."; /** Used for validation. */ @Transient public static final String NAME_REQUIRED = "Organization Name is required."; /** Used for validation. */ @Transient public static final String SHORTNAME_REQUIRED = "Organization Web Address is required."; /** Used for validation. */ public static final String MIN_COORDINATORS_MESSAGE = "Organizations must have at least one coordinator."; /** Used for validation. */ public static final String DESCRIPTION_LENGTH_MESSAGE = "Description supports up to " + MAX_DESCRIPTION_LENGTH + " characters."; /** Used for validation. */ public static final String ALPHA_NUMERIC_PATTERN = "[A-Za-z0-9]*"; /** Used for validation. */ public static final String SHORT_NAME_CHARACTERS = "A short name can only contain alphanumeric " + "characters and no spaces."; /** * Transient 'isPublic' field used only for searching - helps speed up certain queries that contain * permission-scoped entities. */ @Transient @Field(name = "isPublic", index = Index.UN_TOKENIZED, store = Store.NO) @SuppressWarnings("unused") private final boolean publicGroup = true; // TODO why is this called publicgroup? /** * The name of the organization. */ @Basic(optional = false) @Length(min = 1, max = MAX_NAME_LENGTH, message = NAME_LENGTH_MESSAGE) @Fields(value = { @Field(name = "name", index = Index.TOKENIZED, store = Store.NO, // use Text stemmer analyzer analyzer = @Analyzer(impl = TextStemmerAnalyzer.class)), // sort field - needs to be indexed and we need the StandardAnalyzer to convert to lowercase, and to pull // out common words and punctuation. This way "The XYZ" will show up after "XYZ" @Field(name = "byName", index = Index.UN_TOKENIZED, store = Store.NO, // bridge tokenizes then joins bridge = @FieldBridge(impl = StandardAnalyzerSortFieldBridge.class)) }) private String name; /** * The skills that are contained in this background. */ @OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.ALL }) @JoinTable(name = "Organization_Capability", // join columns joinColumns = { @JoinColumn(table = "Organization", name = "organizationId") }, // inverse join columns inverseJoinColumns = { @JoinColumn(table = "BackgroundItem", name = "capabilityId") }, // unique constraints uniqueConstraints = { @UniqueConstraint(columnNames = { "organizationId", "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; /** * The short version of organization name. */ @Column(nullable = false, unique = true) @Length(min = 1, max = MAX_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 organization. */ @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 url of the organization. */ @Basic @Pattern(regex = URL_REGEX_PATTERN, message = WEBSITE_MESSAGE) private String url; /** * if all users can create groups under this org. */ @Basic private Boolean allUsersCanCreateGroups = true; /** * The X coordinate of the upper left corner of the crop. */ @Basic private Integer avatarCropX; /** * The Y coordinate of the upper left corner of the crop. */ @Basic private Integer avatarCropY; /** * The width of the crop. */ @Basic private Integer avatarCropSize; /** * avatar id image for this user. */ @Basic private String avatarId; /** * The description of the organization. */ @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; /** * List of coordinators for this organization. */ @Size(min = 1, message = MIN_COORDINATORS_MESSAGE) @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.REFRESH }) // LazyCollectionOption.EXTRA is only being used here as a workaround for // hibernate bug where lazy collection collide with validators. This should // NOT be used in any other situation without understanding full ramifications // as it does potentially involve extra queries. @LazyCollection(LazyCollectionOption.EXTRA) @JoinTable(name = "Organization_Coordinators") private Set<Person> coordinators; /** * Get the parent org id w/o loading the org. */ @Formula("parentOrganizationId") private Long parentOrgId; /** * Private collection mapped for JPA queries. */ @SuppressWarnings("unused") @OneToMany(fetch = FetchType.LAZY) @Where(clause = "(id <> parentOrganizationId)") @JoinColumn(name = "parentOrganizationId") private Set<Organization> childOrganizations; /** * The persons that have this org as realated org. NOTE: This is a private lazy collection that allow * Person_RelatedOrganization to be cleaned up when an org is deleted. It is not for external use. */ @SuppressWarnings("unused") @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "Person_RelatedOrganization", // join columns inverseJoinColumns = { @JoinColumn(table = "Person", name = "personId") }, // inverse join columns joinColumns = { @JoinColumn(table = "Organization", name = "organizationId") }, // unique constraints uniqueConstraints = { @UniqueConstraint(columnNames = { "personId", "organizationId" }) }) private final List<Person> relatedPersons = new ArrayList<Person>(); /** * The de-normalized child (non-recursive) organization count. */ @Basic(optional = false) private int childOrganizationCount = 0; /** * The de-normalized descendant (recursive) group count. */ @Basic(optional = false) private int descendantGroupCount = 0; /** * The number of updates for this org. */ @Basic(optional = false) private int updatesCount = 0; /** * The de-normalized count (recursive) of employees in this organization. */ @Basic(optional = false) private int descendantEmployeeCount = 0; /** * The de-normalized count of employees following this organization. */ @Basic(optional = false) private int employeeFollowerCount = 0; /** * 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 organization. */ @OneToOne(fetch = FetchType.EAGER, cascade = { CascadeType.ALL }) @JoinColumn(name = "streamScopeId") private StreamScope streamScope; /** * Default constructor. */ public Organization() { // no-op } /** * Constructor that creates a skeleton org from the input OrganizationModelView, populating the fields that the * front-end typically needs. * * - orgId, shortName, name, bannerId * * @param inOrgModelView * - the organization modelview to pull values from */ public Organization(final OrganizationModelView inOrgModelView) { setId(inOrgModelView.getEntityId()); setShortName(inOrgModelView.getShortName()); setName(inOrgModelView.getName()); setBannerId(inOrgModelView.getBannerId()); setParentOrgId(inOrgModelView.getParentOrganizationId()); } /** * Override equality to be based on the org's id. * * @param rhs * target object * @return true if equal, false otherwise. */ @Override public boolean equals(final Object rhs) { return (rhs instanceof Organization && this.getId() == ((Organization) rhs).getId()); } /** * set the id - useful for unit testing. * * @param newId * the new id */ @Override protected void setId(final long newId) { super.setId(newId); } /** * 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; } /** * List of leaders for this organization. */ @ManyToMany(fetch = FetchType.LAZY) @JoinTable(name = "Organization_Leaders") private Set<Person> leaders; /** * Parent organization - note: this is indexed as a class-level bridge. */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "parentOrganizationId") @Fields(value = { // "parentOrganizationShortName" @Field(name = "parentOrganizationShortName", index = Index.UN_TOKENIZED, store = Store.NO, // field bridge bridge = @FieldBridge(impl = OrganizationToShortNameFieldBridge.class)), // "parentOrganizationShortNameHierarchy" - a space-separated list of all short names up the tree @Field(name = "parentOrganizationIdHierarchy", index = Index.TOKENIZED, store = Store.NO, // WhitespaceAnalyzer to split on spaces, not lowercase, and not use stop words - necessary to mention // since we're tokenizing analyzer = @Analyzer(impl = WhitespaceAnalyzer.class), // field bridge bridge = @FieldBridge(impl = OrgIdHierarchyFieldBridge.class)) }) private Organization parentOrganization; /** * Constructor. This should have every non-null parameter included. * * @param inName * - Full name of org. * @param inShortName * Short name of org. */ public Organization(final String inName, final String inShortName) { name = inName; setShortName(inShortName); } /** * Add coordinator to org. * * @param person * The Person to add. */ public void addCoordinator(final Person person) { if (coordinators == null) { // doesn't *need* to be TreeSet, I just picked it. coordinators = new TreeSet<Person>(); } coordinators.add(person); } /** * Remove coordinator from org. * * @param person * The Person to remove. */ public void removeCoordinator(final Person person) { for (Person p : coordinators) { if (person.getId() == p.getId()) { coordinators.remove(p); break; } } } /** * 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; } /** * Add leaders to org. * * @param person * The Person to add. */ public void addLeader(final Person person) { leaders.add(person); } /** * Remove leader from org. * * @param person * The Person to remove. */ public void removeLeader(final Person person) { for (Person p : leaders) { if (person.getId() == p.getId()) { leaders.remove(p); break; } } } /** * Getter for list of leaders. * * @return list of leaders. */ public Set<Person> getLeaders() { return leaders; } /** * Setter for list of leaders. * * @param inLeaders * list of coordinators. */ public void setLeaders(final Set<Person> inLeaders) { leaders = inLeaders; } /** * Getter for Organization name. * * @return the name. */ public String getName() { return name; } /** * Setter for Organization name. * * @param inName * new name */ public void setName(final String inName) { name = (null == inName) ? "" : inName; } /** * Getter for Organization short name. * * @return the shortName */ public String getShortName() { return shortName; } /** * Setter for Organization short name. * * @param inShortName * the shortName to set. */ public void setShortName(final String inShortName) { shortName = (null == inShortName) ? "" : inShortName.toLowerCase(); } /** * @return the url */ public String getUrl() { return url; } /** * @param inUrl * the url to set */ public void setUrl(final String inUrl) { url = inUrl; } /** * @return the description */ public String getDescription() { return description; } /** * @param inDescription * the description to set */ public void setDescription(final String inDescription) { description = inDescription; } /** * @return the parentOrganization */ @Override public Organization getParentOrganization() { return parentOrganization; } /** * Parent Organization - used for serialization and unit testing only - setting a parent organization must be done * through the mapper. * * @param inParentOrganization * the parentOrganization to set */ @Override public void setParentOrganization(final Organization inParentOrganization) { parentOrganization = inParentOrganization; } /** * @return If this organization is known to be the root org. */ public boolean isRootOrganization() { return parentOrganization != null && parentOrganization.getId() == getId(); } /** * check to see if the specified account id is a coordinator for this Organization. * * @param account * to check. * @return if they're a coordinator. */ public boolean isCoordinator(final String account) { for (Person p : coordinators) { if (p.getAccountId().equals(account)) { return true; } } return false; } /** * 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 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; } /** * Set the number of child (non-recursive) organizations. * * @param inCount * the count to set */ public void setChildOrganizationCount(final int inCount) { childOrganizationCount = inCount; } /** * Get the number of child (non-recursive) organizations - de-normalized. * * @return the count */ public int getChildOrganizationCount() { return childOrganizationCount; } /** * Get the de-normalized count of employees in this organization. * * @return the employeeCount */ public int getDescendantEmployeeCount() { return descendantEmployeeCount; } /** * Set the employee count. * * @param inDescendantEmployeeCount * the employeeCount to set */ public void setDescendantEmployeeCount(final int inDescendantEmployeeCount) { descendantEmployeeCount = inDescendantEmployeeCount; } /** * Set the de-normalized number of employees following this organization - for unit testing and serialization. * * @param inEmployeeFollowerCount * the employeeFollowerCount to set */ protected void setEmployeeFollowerCount(final int inEmployeeFollowerCount) { employeeFollowerCount = inEmployeeFollowerCount; } /** * Get the de-normalized number of employees following this organization. * * @return the employeeFollowerCount the number of employees following this organization */ public int getEmployeeFollowerCount() { return employeeFollowerCount; } /** * Set the de-normalized group count - for serialization and unit testing. * * @param inDescendantGroupCount * the groupCount to set */ public void setDescendantGroupCount(final int inDescendantGroupCount) { descendantGroupCount = inDescendantGroupCount; } /** * Get the de-normalized group count. * * @return the groupCount */ public int getDescendantGroupCount() { return descendantGroupCount; } /** * @return the banner Id */ @Override public String getBannerId() { return bannerId; } /** * @param inBannerId * the banner to set */ @Override public void setBannerId(final String inBannerId) { bannerId = inBannerId; } /** * Set the number of updates for the org. * * @param inUpdatesCount * the updatesCount to set */ public void setUpdatesCount(final int inUpdatesCount) { updatesCount = inUpdatesCount; } /** * Get the number of updates for the org. * * @return the updatesCount */ public int getUpdatesCount() { return updatesCount; } /** * 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; } /** * @param inAllUsersCanCreateGroups * the allUsersCanCreateGroups to set. */ public void setAllUsersCanCreateGroups(final Boolean inAllUsersCanCreateGroups) { allUsersCanCreateGroups = inAllUsersCanCreateGroups; } /** * @return the allUsersCanCreateGroups. */ public Boolean getAllUsersCanCreateGroups() { return allUsersCanCreateGroups; } // ---------------------------------------------------- // ------------------ 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<Organization> entityCacheUpdater; /** * Setter for the static PersonUpdater. * * @param inEntityCacheUpdater * the PersonUpdater to set */ public static void setEntityCacheUpdater(final EntityCacheUpdater<Organization> 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; } /** * Get the parent org id without loading the parent organization. * * @return the parent org id without loading the parent organization */ @Override public Long getParentOrgId() { return parentOrgId; } /** * Set the parent org id. * * @param inParentOrgId * the parent org id */ protected void setParentOrgId(final Long inParentOrgId) { parentOrgId = inParentOrgId; } /** * {@inheritDoc}. */ @Override public Long getBannerEntityId() { return bannerEntityId; } /** * {@inheritDoc}. */ @Override public void setBannerEntityId(final Long inBannerEntityId) { bannerEntityId = inBannerEntityId; } }