# CommunityMembership.java

    /*
     * Copyright (C) 2000 - 2026 Silverpeas
     *
     * This program is free software: you can redistribute it and/or modify
     * it under the terms of the GNU Affero General Public License as
     * published by the Free Software Foundation, either version 3 of the
     * License, or (at your option) any later version.
     *
     * As a special exception to the terms and conditions of version 3.0 of
     * the GPL, you may redistribute this Program in connection with Free/Libre
     * Open Source Software ("FLOSS") applications as described in Silverpeas's
     * FLOSS exception.  You should have received a copy of the text describing
     * the FLOSS exception, and it is also available here:
     * "https://www.silverpeas.com/legal/licensing"
     *
     * This program is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     * GNU Affero General Public License for more details.
     *
     * You should have received a copy of the GNU Affero General Public License
     * along with this program.  If not, see <https://www.gnu.org/licenses/>.
     */
    package org.silverpeas.components.community.model;

    import org.silverpeas.components.community.repository.CommunityMembershipRepository;
    import org.silverpeas.core.admin.BaseRightProfile;
    import org.silverpeas.core.admin.user.model.SilverpeasRole;
    import org.silverpeas.core.admin.user.model.User;
    import org.silverpeas.core.persistence.Transaction;
    import org.silverpeas.core.persistence.datasource.model.identifier.UuidIdentifier;
    import org.silverpeas.core.persistence.datasource.model.jpa.SilverpeasJpaEntity;

    import jakarta.annotation.Nonnull;
    import jakarta.persistence.*;
    import jakarta.validation.constraints.NotNull;
    import java.time.Instant;
    import java.time.OffsetDateTime;
    import java.time.ZoneOffset;
    import java.util.stream.Collectors;

    /**
     * Membership of a given user in a community of users. Memberships are managed by the
     * {@link CommunityOfUsers} to which they are related, and they are provided by the
     * {@link CommunityMembershipsProvider} which ensures the synchronization between the memberships to
     * a community and the users playing a role in the community space.
     * <p>
     * All along the life of his membership, the status of his membership in the community can change.
     * When a community requires a validation step for memberships requests, the user asking such a
     * thing has his membership created with a pending status. Otherwise, his membership is committed
     * automatically. Once his membership committed, a user becomes then a member of the community of
     * users. This means he has rights to access the content of the community space, which is a
     * collaborative space with a community of users. To become a member of a community for a space, a
     * user has to ask to join this community to the administrators. As a member of a community of a
     * space, the user can navigate within the space's tree according to his access rights. A user is
     * said to be a member of a given community space if and only if he plays a role in this space.
     * </p>
     *
     * @author mmoquillon
     */
    @Entity
    @Table(name = "SC_Community_Membership")
    @NamedQuery(name = "byUserIdAndByCommunity",
        query = "select m from CommunityMembership m where m.userId = :userId and m.community = " +
            ":community and (m.status = org.silverpeas.components.community.model.MembershipStatus" +
            ".COMMITTED or m.status = org.silverpeas.components.community.model.MembershipStatus" +
            ".PENDING)")
    @NamedQuery(name = "allNonRemoved",
        query =
            "select m from CommunityMembership m where m.community = :community and m.status <> org" +
                ".silverpeas.components.community.model.MembershipStatus.REMOVED order by m" +
                ".lastUpdateDate desc")
    public class CommunityMembership extends SilverpeasJpaEntity<CommunityMembership, UuidIdentifier> {

      @OneToOne(fetch = FetchType.EAGER, optional = false)
      @JoinColumn(name = "community", referencedColumnName = "id")
      @NotNull
      private CommunityOfUsers community;
      @Column(nullable = false)
      @NotNull
      private int userId;
      @Column(nullable = false)
      @Enumerated(EnumType.STRING)
      @NotNull
      private MembershipStatus status;
      private Instant joiningDate;

      private transient User user;

      /**
       * Sets the specified user as a member of the specified community and gets his resulting
       * membership. The user isn't actually a member of the given community. For doing, invoke the
       * {@link CommunityOfUsers#addAsMember(User, SilverpeasRole)} method. This method is to be used by
       * the {@link CommunityOfUsers} instances.
       *
       * @param user the user to get as a member.
       * @param community the community for which the user has to be a member.
       * @return the membership of the given user to the specified community.
       */
      static CommunityMembership asMember(final User user, final CommunityOfUsers community) {
        CommunityMembership member = new CommunityMembership();
        member.user = user;
        member.userId = Integer.parseInt(user.getId());
        member.community = community;
        member.status = MembershipStatus.COMMITTED;
        return member;
      }

      /**
       * Constructs an empty member of nothing. To be used by the persistence engine when fetching
       * members objects from the database.
       */
      protected CommunityMembership() {
        // for JPA
      }

      /**
       * Gets the community to which this membership belongs.
       *
       * @return the community of this user.
       */
      public @Nonnull CommunityOfUsers getCommunity() {
        return community;
      }

      /**
       * Gets the user related to this membership.
       *
       * @return the user related to this membership
       */
      public @Nonnull User getUser() {
        if (user == null) {
          user = User.getById(String.valueOf(userId));
        }
        return user;
      }

      /**
       * Gets the current status of this membership.
       *
       * @return the membership status.
       */
      public @Nonnull MembershipStatus getStatus() {
        return status;
      }

      /**
       * Gets the date at which the user has effectively joined the community. The date at which his
       * membership has been committed. If the membership of the user to the community hasn't yet been
       * committed, then null is returned.
       *
       * @return the date and time in UTC or null if this membership hasn't been committed.
       */
      public OffsetDateTime getJoiningDate() {
        return joiningDate == null ? null : OffsetDateTime.ofInstant(joiningDate, ZoneOffset.UTC);
      }

      /**
       * Gets the role the user related by this membership plays in the community space. In the case the
       * user plays several roles, only the highest one is returned.
       *
       * @return the (highest) role the user plays in the community space. If the user isn't more a
       * member of the community, then null is returned.
       */
      public SilverpeasRole getMemberRole() {
        var roles =
            community.getCommunitySpace().getAllSpaceProfilesOfUser(String.valueOf(userId)).stream()
                .map(BaseRightProfile::getName)
                .map(SilverpeasRole::fromString)
                .collect(Collectors.toSet());
        return roles.isEmpty() ? null : SilverpeasRole.getHighestFrom(roles);
      }

      /**
       * Sets the new specified status to this membership to the underlying community of users. A member
       * of a community is never deleted from the table of members in the data storage. When he's
       * removed from the community, his status in the data storage is just updated to
       * {@link MembershipStatus#REMOVED}. Only the {@link CommunityOfUsers} instances should use this
       * method as their goal is also to control the membership of a user to them.
       *
       * @param newStatus the new membership status.
       */
      void setStatus(final MembershipStatus newStatus) {
        this.status = newStatus;
      }

      /**
       * Saves the state of this membership into the table of memberships of the underlying community of
       * users. If such membership already exists in the table, and it hasn't been removed, then its
       * state is then just updated. In the case this member related by this membership has been removed
       * from the community, an {@link IllegalStateException} is thrown. To delete the membership of a
       * user in the community, please use instead the {@link CommunityMembership#delete()} method. This
       * method is to be used by the {@link CommunityOfUsers} and the
       * {@link CommunityMembershipsProvider} instances.
       *
       * @throws IllegalStateException if the status of this membership is
       * {@link MembershipStatus#REMOVED} as a removed membership cannot be anymore modified.
       */
      void save() {
        if (status == MembershipStatus.REMOVED) {
          throw new IllegalStateException(
              "The user isn't anymore member of the community and as such its state cannot be updated");
        }
        Transaction.performInOne(() -> {
          if (status == MembershipStatus.COMMITTED && joiningDate == null) {
            joiningDate = OffsetDateTime.now(ZoneOffset.UTC).toInstant();
          }
          CommunityMembershipRepository.get().save(this);
          return null;
        });
      }

      /**
       * Deletes the membership of the user in the underlying community of users. The membership of the
       * user won't be effectively deleted in the table of memberships of the underlying community of
       * users; instead his membership status will pass to {@link MembershipStatus#REMOVED} before being
       * saved as such in the table of memberships. This method is to be used by the
       * {@link CommunityOfUsers} and the {@link CommunityMembershipsProvider} instances.
       *
       * @throws IllegalStateException if the status of this membership is
       * {@link MembershipStatus#PENDING} as a pending membership cannot be removed but only refused or
       * accepted.
       */
      void delete() {
        if (status == MembershipStatus.PENDING) {
          throw new IllegalStateException(
              "A pending membership of a user in the community cannot be removed! It can be only " +
                  "refused or validated");
        }
        Transaction.performInOne(() -> {
          status = MembershipStatus.REMOVED;
          CommunityMembershipRepository.get().save(this);
          return null;
        });
      }

      @Override
      public boolean equals(final Object obj) {
        return super.equals(obj);
      }

      @Override
      public int hashCode() {
        return super.hashCode();
      }

    }