CommunityOfUsers.java

/*
 * Copyright (C) 2000 - 2024 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.AlreadyMemberException;
import org.silverpeas.components.community.repository.CommunityOfUsersRepository;
import org.silverpeas.core.admin.component.model.InheritableSpaceRoles;
import org.silverpeas.core.admin.service.AdminException;
import org.silverpeas.core.admin.service.Administration;
import org.silverpeas.core.admin.space.SpaceHomePageType;
import org.silverpeas.core.admin.space.SpaceInst;
import org.silverpeas.core.admin.space.SpaceProfileInst;
import org.silverpeas.core.admin.user.model.GroupDetail;
import org.silverpeas.core.admin.user.model.SilverpeasRole;
import org.silverpeas.core.admin.user.model.User;
import org.silverpeas.core.contribution.content.wysiwyg.service.WysiwygController;
import org.silverpeas.core.contribution.model.WysiwygContent;
import org.silverpeas.core.persistence.Transaction;
import org.silverpeas.core.persistence.datasource.model.identifier.UuidIdentifier;
import org.silverpeas.core.persistence.datasource.model.jpa.BasicJpaEntity;
import org.silverpeas.core.security.authorization.AccessControlContext;
import org.silverpeas.core.security.authorization.ComponentAccessControl;
import org.silverpeas.kernel.SilverpeasRuntimeException;
import org.silverpeas.kernel.annotation.NonNull;
import org.silverpeas.kernel.bundle.ResourceLocator;
import org.silverpeas.kernel.bundle.SettingBundle;
import org.silverpeas.kernel.util.Pair;

import javax.annotation.Nonnull;
import javax.persistence.*;
import javax.validation.constraints.NotNull;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.stream.Stream;

import static java.util.Optional.ofNullable;
import static java.util.function.Predicate.not;
import static org.silverpeas.core.admin.space.SpaceHomePageType.STANDARD;
import static org.silverpeas.kernel.util.StringUtil.EMPTY;

/**
 * The community of users for a collaborative space. Users in the community are said to be members
 * of this community, and hence of the space for which the community has been spawned. The space is
 * then said a community space. A community is always managed by a Community application instance;
 * it is like the space for which the community has been created delegates its management to that
 * application instance. The difference between a community space with a collaborative space is in
 * the former the users ask to join the community and then gain access rights to the space. Another
 * difference is the community space is visible to all users of the platform without requiring this
 * space to be public; they have just a view of it and of its presentation page.
 * <p>
 * The actual members of a community of users are all included in a specific group of users that is
 * automatically created when the community is spawned. This group of members is kept up-to-date
 * with the users playing a role in the community space. The group of members allow to get in one
 * shot all the actual members of the community. To have a glance about the memberships to the
 * community, passed, actual and pending, please asks to the {@link CommunityMembershipsProvider}
 * object associated with the community of users.
 * </p>
 */
@Entity
@Table(name = "SC_Community")
@NamedQuery(
    name = "CommunityByComponentInstanceId",
    query = "select c from CommunityOfUsers c where c.componentInstanceId = :componentInstanceId")
@NamedQuery(
    name = "CommunityBySpaceId",
    query = "select c from CommunityOfUsers c where c.spaceId = :spaceId")
public class CommunityOfUsers
    extends BasicJpaEntity<CommunityOfUsers, UuidIdentifier> {
  private static final long serialVersionUID = -4908726669864467915L;

  @Column(name = "spaceId", nullable = false)
  @NotNull
  private String spaceId;
  @Column(name = "instanceId", nullable = false)
  @NotNull
  private String componentInstanceId;

  Integer groupId;

  private String homePage;

  @Enumerated(EnumType.ORDINAL)
  private SpaceHomePageType homePageType;

  private URL charterURL;

  @Transient
  private transient CommunityMembershipsProvider provider;

  @Transient
  private transient CommunitySpace communitySpace;

  /**
   * Constructs a new empty Community instance.
   */
  protected CommunityOfUsers() {
    // this constructor is for the persistence engine.
  }

  /**
   * Constructs a new Community instance for the specified resource.
   *
   * @param componentInstanceId the unique identifier of a component instance managing the
   * community.
   * @param spaceId the unique identifier of a resource in Silverpeas for which the community is
   * constructed.
   */
  public CommunityOfUsers(final String componentInstanceId, final String spaceId) {
    this.componentInstanceId = componentInstanceId;
    this.spaceId = spaceId;
    this.communitySpace = new CommunitySpace(this);
  }

  /**
   * Gets all the communities of users existing in Silverpeas.
   * @return a list of exiting community of users.
   */
  public static List<CommunityOfUsers> getAll() {
    CommunityOfUsersRepository repository = CommunityOfUsersRepository.get();
    return repository.getAll();
  }

  /**
   * Gets the community of users managed by the specified component instance. If the component
   * instance doesn't exist then nothing is returned.
   *
   * @param instanceId the unique identifier of a Community application instance.
   * @return maybe a community instance or nothing if the component instance doesn't exist.
   */
  public static Optional<CommunityOfUsers> getByComponentInstanceId(final String instanceId) {
    CommunityOfUsersRepository repository = CommunityOfUsersRepository.get();
    return repository.getByComponentInstanceId(instanceId);
  }

  /**
   * Gets the community of users of the specified collaborative space. Nothing is returned if either
   * the space doesn't exist or it isn't a community space.
   *
   * @param spaceId the unique identifier of a space in Silverpeas.
   * @return maybe a community instance or nothing if there is no community for the specified space.
   */
  public static Optional<CommunityOfUsers> getBySpaceId(final String spaceId) {
    CommunityOfUsersRepository repository = CommunityOfUsersRepository.get();
    return repository.getBySpaceId(spaceId);
  }

  /**
   * Gets the rich content of the presentation of the community space. A presentation of the parent
   * space can be defined in order for users out of the community to have a glance of what the
   * community space is about. The goal is to give them enough information for deciding to join the
   * community of the space.
   *
   * @return a {@link WysiwygContent} instance.
   */
  public WysiwygContent getSpacePresentationContent() {
    return WysiwygController.get(getComponentInstanceId(), "SpaceFacade", null);
  }

  public String getComponentInstanceId() {
    return componentInstanceId;
  }

  /**
   * Gets the unique identifier of the community space, that is to say the collaborative space for
   * which this community of users is.
   *
   * @return the unique identifier of a space in Silverpeas.
   */
  public String getSpaceId() {
    return spaceId;
  }

  /**
   * Checks the given user is a member of this community of users by verifying he's playing a role
   * in the community space.
   *
   * @param user {@link User} instance.
   * @return true whether the user is a member of this community, false otherwise.
   * @implSpec a user is member of the community if and only if he plays a non-inherited role in the
   * parent space (the community space) among the following ones: {@link SilverpeasRole#ADMIN},
   * {@link SilverpeasRole#PUBLISHER}, {@link SilverpeasRole#WRITER} and
   * {@link SilverpeasRole#READER}.
   */
  public boolean isMember(final User user) {
    return getCommunitySpace().getAllUsers().contains(user.getId());
  }

  /**
   * Gets the roles the given user has on the given community.
   *
   * @param user {@link User} instance.
   * @return an unmodifiable set of {@link SilverpeasRole}.
   */
  public Set<SilverpeasRole> getUserRoles(final User user) {
    return Set.copyOf(ComponentAccessControl.get()
        .getUserRoles(user.getId(), getComponentInstanceId(), AccessControlContext.init()));
  }

  /**
   * Gets the home page of this community of users for its members.
   *
   * @return the home page of the community of users for the members.
   */
  public Pair<String, SpaceHomePageType> getHomePage() {
    return Pair.of(ofNullable(homePage).orElse(EMPTY), ofNullable(homePageType).orElse(STANDARD));
  }

  /**
   * Gets the URL at which the charter (or a community guide) is located. The charter has to be
   * validated by a user in order to join the community of users.
   *
   * @return the URL of the charter.
   */
  public URL getCharterURL() {
    return charterURL;
  }

  /**
   * Sets the URL at which the charter (or a community guide) is located. The charter, once set, has
   * to be validated by a user in order to join the community of users.
   *
   * @param charterURL the URL of the charter to set.
   */
  public void setCharterURL(@Nonnull final URL charterURL) {
    Objects.requireNonNull(charterURL);
    this.charterURL = charterURL;
  }

  /**
   * Sets the URL at which the charter (or a community guide) is located. The charter, once set, has
   * to be validated by a user in order to join the community of users.
   *
   * @param charterURL the URL of the charter to set.
   * @throws MalformedURLException if the specified URL is malformed.
   */
  public void setCharterURL(final String charterURL) throws MalformedURLException {
    this.charterURL = new URL(charterURL);
  }

  /**
   * Unsets the charter.
   *
   * @see #setCharterURL(String)
   */
  public void unsetCharterURL() {
    this.charterURL = null;
  }

  /**
   * y Sets the home page of this community of users to render to the members.
   *
   * @param homePage the home page of the community of users.
   * @param homePageType the type of the home page.
   */
  public void setHomePage(final String homePage, SpaceHomePageType homePageType) {
    this.homePage = homePage;
    this.homePageType = homePageType;
  }

  /**
   * Adds the specified user as a member pending his membership to this community to be committed.
   *
   * @param user the user to add as a pending member.
   * @return the pending membership of the user.
   */
  public CommunityMembership addAsAPendingMember(final User user) {
    checkAlreadyMember(user);
    getMembershipsProvider().get(user)
        .map(CommunityMembership::getStatus)
        .filter(s -> s == MembershipStatus.PENDING)
        .ifPresent(s -> {
          throw new AlreadyMemberException(
              "The membership of the user " + user.getId() + " is already pending!");
        });
    return Transaction.performInOne(() -> {
      CommunityMembership membership = CommunityMembership.asMember(user, this);
      membership.setStatus(MembershipStatus.PENDING);
      membership.save();
      return membership;
    });
  }

  /**
   * Adds the specified user as a member of this community of users and with the specified role. In
   * the case a membership application is pending for the user, his membership is then validated,
   * and he's added in the specifying role in the community space.
   *
   * @param user the user to add as a committed member.
   * @param role the role the user should play in the community.
   * @return the committed membership of the user.
   * @throws SilverpeasRuntimeException if an unexpected error occurs while adding the specified
   * user in this community with the given role.
   */
  public CommunityMembership addAsMember(final User user, final SilverpeasRole role) {
    if (!InheritableSpaceRoles.isASpaceRole(role)) {
      throw new IllegalArgumentException("The role " + role.getName() + " isn't a role of a space");
    }
    checkAlreadyMember(user);
    return Transaction.performInOne(() -> {
      getCommunitySpace().addUser(user, role);
      return commitMembership(user);
    });
  }

  /**
   * Refuses the membership application of the specified user to this community of users. For doing,
   * the user must have a membership application pending for validation, otherwise an
   * {@link IllegalStateException} is thrown.
   *
   * @param user the user for whom his membership application is refused.
   * @return the refused membership of the user to this community of users.
   */
  public CommunityMembership refuseMembership(final User user) {
    CommunityMembership membership = getMembershipsProvider().get(user)
        .filter(m -> m.getStatus().isPending())
        .orElseThrow(() -> new IllegalStateException(
            "The user " + user.getId() + " has no pending membership to the community " +
                getId()));
    return Transaction.performInOne(() -> {
      membership.setStatus(MembershipStatus.REFUSED);
      membership.save();
      return membership;
    });
  }

  /**
   * Removes the membership of the specified user to this community of users. The membership of the
   * user isn't actually deleted; only his membership status is updated to
   * {@link MembershipStatus#REMOVED}. Once removed, a user isn't anymore member of the community
   * and hence his membership status cannot be updated. In others words, when such a user is added
   * again among the members of the community, a new entry in the memberships table of the community
   * is created for this user; he has a new membership data.
   *
   * @param user the user to remove from this community.
   * @return the removed membership with the status updated or null if the given user isn't member
   * of this community.
   * @implSpec the user will be removed from all the roles of the parent space (the community space)
   * and then his membership status is updated to {@link MembershipStatus#REMOVED}.
   */
  public CommunityMembership removeMembership(final User user) {
    return Transaction.performInOne(() -> {
      getCommunitySpace().removeUser(user);
      return getMembershipsProvider().get(user)
          .map(m -> {
            m.delete();
            return m;
          })
          .orElse(null);
    });
  }

  /**
   * Gets all the memberships to this community of users.
   *
   * @return a provider of memberships to this community with which fine requests can be invoked to
   * get some parts of the memberships of the users to this community.
   */
  public CommunityMembershipsProvider getMembershipsProvider() {
    if (provider == null) {
      provider = CommunityMembershipsProvider.getProvider(this);
    }
    return provider;
  }

  /**
   * Saves the modification in this community of users. If the community isn't a persisted one, then
   * an {@link IllegalStateException} exception is thrown.
   */
  public void save() {
    if (!isPersisted()) {
      throw new IllegalStateException("This community isn't a persisted one!");
    }
    Transaction.performInOne(() -> {
      CommunityOfUsersRepository.get().save(this);
      return null;
    });
  }

  /**
   * Deletes this community of users. The memberships to this community should be removed before.
   * Take care this deletion will be definitely. This method should be used only by inner
   * administration tasks.
   */
  public void delete() {
    Transaction.performInOne(() -> {
      var repository = CommunityOfUsersRepository.get();
      repository.delete(this);
      // to ensure the community is removed (and hence its link to the group) before deleting the
      // referenced group
      repository.flush();
      getCommunitySpace().deleteMembersGroup();
      return null;
    });
  }

  /**
   * Gets the group of all the members of this community of users. If this community isn't
   * persisted, null is returned. Otherwise the group of members of this community of users is
   * returned. In the case such a group isn't yet created, then this method will create it before
   * returning it. If no users are currently members in this community, then the returned group of
   * users will be empty.
   *
   * @return the group of users who are all members of this community or null if this community
   * isn't yet persisted.
   * @throws SilverpeasRuntimeException if an error occurs while creating or getting the group of
   * members.
   */
  public GroupDetail getGroupOfMembers() {
    if (!isPersisted()) {
      return null;
    }
    GroupDetail group;
    try {
      if (groupId == null) {
        SpaceInst spaceInst = getCommunitySpace().getSilverpeasSpace();
        group = getCommunitySpace().createMembersGroup(spaceInst);
      } else {
        group = getCommunitySpace().getMembersGroup();
      }
    } catch (AdminException e) {
      throw new SilverpeasRuntimeException(e);
    }
    return group;
  }

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

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

  /**
   * Gets the Silverpeas space mapped with this community of users.
   *
   * @return the Silverpeas space as a community space.
   */
  CommunitySpace getCommunitySpace() {
    if (communitySpace == null) {
      communitySpace = new CommunitySpace(this);
    }
    return communitySpace;
  }

  private CommunityMembership commitMembership(User user) {
    var mayBeMember = getMembershipsProvider().get(user);
    CommunityMembership
        m = mayBeMember
        .filter(mb -> mb.getStatus().isPending())
        .orElseGet(() -> CommunityMembership.asMember(user, this));
    m.setStatus(MembershipStatus.COMMITTED);
    m.save();
    return m;
  }

  private void synchronizeRemovingIfAny(final User user) {
    Transaction.performInOne(() -> {
      getMembershipsProvider().get(user)
          .filter(m -> m.getStatus().isMember())
          .ifPresent(CommunityMembership::delete);
      return null;
    });
  }

  private void checkAlreadyMember(final User user) {
    if (isMember(user)) {
      throw new AlreadyMemberException(
          "User " + user.getId() + " is already a member of the community " + getId());
    } else {
      synchronizeRemovingIfAny(user);
    }
  }

  /**
   * A community space. It is a Silverpeas space which represents a community and for which a group
   * of users is maintained for the members of the community. The community of users is ruled by the
   * community application instanciated in this same space. Any users playing a role in the space is
   * considered as a member of the community and each member of a community has to play at least one
   * role in the space. As such, a member of a community should be a user in the members group. It
   * is the responsibility of the {@link CommunityMembershipsProvider}, from which memberships can
   * be got, to ensure users playing a role in the space are members of the community and are in the
   * members group.
   * <p>
   * A community space is responsible to manage both the user profiles for the space related by a
   * community of users, and the members group associated to this space.
   * </p>
   */
  static class CommunitySpace {

    private static final SettingBundle settings = ResourceLocator.getSettingBundle(
        "org.silverpeas.components.community.settings.communitySettings");

    private final Administration administration;
    private final User requester;
    private final CommunityOfUsers community;

    /**
     * Creates a new community space representing the Silverpeas space mapped with the specified
     * community of users.
     *
     * @param community the community of users underlying to this community space.
     */
    public CommunitySpace(CommunityOfUsers community) {
      this.community = community;
      administration = Administration.get();
      requester = User.getCurrentRequester();
    }

    /**
     * Adds the specified user into the this community space with the provided role.
     *
     * @param user the user to add in the community space as member.
     * @param role the role the user will play in the community space.
     * @throws SilverpeasRuntimeException if an unexpected error occurs while adding the user in the
     * community space.
     * @implNote the adding of a user in the community space consists of adding him both into the
     * corresponding user profile of the space (identified by the Silverpeas role) and into the
     * group of users dedicated to the members of the community space. If the group doesn't yet
     * exist, then it is created.
     */
    public void addUser(@NonNull User user, @NonNull SilverpeasRole role) {
      Objects.requireNonNull(user);
      Objects.requireNonNull(role);
      execute(() -> {
        var space = getSilverpeasSpace();
        var profile = getSpaceProfile(role, space);
        addUserInSpaceProfile(user, profile);
        addUserInMembershipGroup(user, space);
      });
    }

    /**
     * Removes the specified user from this community space.
     *
     * @param user the user to remove.
     * @implNote the user is both removed from any user profile of the space and from the user group
     * of all the members of the community. If the user to remove doesn't play any role in the
     * community space or if the members group isn't yet created, then no remove operation relative
     * to these resources is done.
     */
    public void removeUser(@NonNull User user) {
      Objects.requireNonNull(user);
      execute(() -> {
        var space = getSilverpeasSpace();
        removeUserFromAllSpaceProfiles(user, space);
        removeUserFromMembershipGroup(user);
      });
    }

    /**
     * Deletes definitely the members group associated with this community space. This method is to
     * be invoked in community deletion. If no members group has been created before the deletion of
     * a space, then nothing is done.
     */
    private void deleteMembersGroup() {
      execute(() -> {
        var group = getMembersGroup();
        if (group != null) {
          administration.deleteGroupById(group.getId(), true);
        }
      });
    }

    /**
     * Gets the unique identifier of all the users playing a role in this space. Those users should
     * be a member of the corresponding community of users. Nevertheless, be aware a lag can exist
     * between the users in the space and their memberships in the community of users.
     *
     * @return a set of user identifiers. The set is empty is there is no yet users playing a role
     * in the community space.
     */
    public Set<String> getAllUsers() {
      Set<String> users = new HashSet<>();
      execute(() -> {
        SpaceInst space = getSilverpeasSpace();
        streamOnNonInheritedSpaceProfiles(space)
            .flatMap(p -> p.getAllUsers().stream())
            .forEach(users::add);
      });
      // now we ensure the members group is up-to-date with the users playing a role in the
      // community space
      var spaceSynchro = community.getCommunitySpace().getSynchronizationTask();
      spaceSynchro.synchronizeMembersGroup(users);
      return users;
    }

    /**
     * Gets all the profiles the specified user has in the community space.
     *
     * @param userId the unique identifier of a user.
     * @return a set with all the roles the user play in community space.
     */
    public Set<SpaceProfileInst> getAllSpaceProfilesOfUser(String userId) {
      Set<SpaceProfileInst> profiles = new HashSet<>();
      execute(() -> {
        SpaceInst space = getSilverpeasSpace();
        streamOnNonInheritedSpaceProfiles(space)
            .filter(p -> p.getAllUsers().contains(userId))
            .forEach(profiles::add);
      });
      return profiles;
    }

    /**
     * Gets a synchronization task related to ensure the community space data are up-to-date with
     * the membership of a user. This method is for the {@link CommunityMembershipsProvider}.
     *
     * @return a synchronization task for this community space.
     */
    private SynchronizationTask getSynchronizationTask() {
      return new SynchronizationTask();
    }

    private SpaceProfileInst getSpaceProfile(SilverpeasRole role, SpaceInst space)
        throws AdminException {
      var profile = space.getSpaceProfileInst(role.getName());
      if (profile == null) {
        profile = new SpaceProfileInst();
        profile.setName(role.getName());
        profile.setSpaceFatherId(space.getId());
        profile.setInherited(false);
        administration.addSpaceProfileInst(profile, requester.getId());
        space.addSpaceProfileInst(profile);
      }
      return profile;
    }

    private void addUserInSpaceProfile(User user, SpaceProfileInst profile) throws AdminException {
      if (!isUserHasProfile(user, profile)) {
        profile.addUser(user.getId());
        administration.updateSpaceProfileInst(profile, requester.getId());
      }
    }

    private void addUserInMembershipGroup(User user, SpaceInst space) throws AdminException {
      var group = getMembersGroup();
      boolean newGroup = group == null;
      if (newGroup) {
        group = createMembersGroup(space);
      }
      if (newGroup || !isUserInGroup(user, group)) {
        administration.addUserInGroup(user.getId(), group.getId());
      }
    }

    /**
     * Creates the group of the members for the specified community space.
     *
     * @param space an existing community space in Silverpeas
     * @return the created group of members
     * @throws AdminException if an error occurs while creating the group of members.
     */
    private GroupDetail createMembersGroup(SpaceInst space) throws AdminException {
      GroupDetail group;
      group = new GroupDetail();
      String groupName = settings.getString("community.group.symbol", "") + " " +
          space.getName();
      group.setName(groupName.trim());
      community.groupId = Integer.parseInt(administration.addGroup(group, true));
      community.save();
      var profile = getSpaceProfile(SilverpeasRole.READER, space);
      profile.addGroup(group.getId());
      administration.updateSpaceProfileInst(profile, requester.getId());
      return group;
    }

    private void removeUserFromAllSpaceProfiles(User user, SpaceInst space) {
      streamOnNonInheritedSpaceProfiles(space)
          .filter(p -> isUserHasProfile(user, p))
          .forEach(p -> execute(() -> {
            p.removeUser(user.getId());
            administration.updateSpaceProfileInst(p, requester.getId());
          }));
    }

    private Stream<SpaceProfileInst> streamOnNonInheritedSpaceProfiles(SpaceInst space) {
      return space.getAllSpaceProfilesInst().stream()
          .filter(not(SpaceProfileInst::isManager).and(not(SpaceProfileInst::isInherited)));
    }

    private void removeUserFromMembershipGroup(User user) throws AdminException {
      var group = getMembersGroup();
      if (group != null && isUserInGroup(user, group)) {
        administration.removeUserFromGroup(user.getId(), group.getId());
      }
    }

    private GroupDetail getMembersGroup() throws AdminException {
      if (community.groupId == null) {
        return null;
      }
      GroupDetail group = administration.getGroup(String.valueOf(community.groupId));

      // check the symbol for group of members didn't change
      String symbol = settings.getString("community.group.symbol", "") + " ";
      SpaceInst space = getSilverpeasSpace();
      if ((symbol.isBlank() && !group.getName().equals(space.getName())) ||
          (!symbol.isBlank() && !group.getName().startsWith(symbol))) {
        group.setName((symbol + space.getName()).trim());
        administration.updateGroup(group, true);
      }
      return group;
    }

    private SpaceInst getSilverpeasSpace() throws AdminException {
      return administration.getSpaceInstById(community.spaceId);
    }

    private boolean isUserInGroup(User user, GroupDetail group) {
      return List.of(group.getUserIds()).contains(user.getId());
    }

    public boolean isUserHasProfile(User user, SpaceProfileInst profile) {
      return profile.getAllUsers().contains(user.getId());
    }

    private void execute(AdminTask task) {
      try {
        task.perform();
      } catch (AdminException e) {
        throw new SilverpeasRuntimeException("Unexpected error: " + e.getMessage());
      }
    }

    @FunctionalInterface
    private interface AdminTask {

      void perform() throws AdminException;
    }

    /**
     * A synchronization task ensures the members group associated with the community space is
     * up-to-date with all the users playing at least one role in this same community space.
     */
    class SynchronizationTask {

      /**
       * Synchronizes the memberships of the underlying community space so that the members group of
       * this space is up-to-date with the users playing a role in it. If a user playing a role in
       * the space isn't yet in the members group, then he's added in this group. If the a user in
       * the group isn't playing any role in the space, then he's removed from the members group.
       *
       * @param usersPlayingARole a set with the unique identifiers of the users who play a role in
       * the community space.
       */
      void synchronizeMembersGroup(final Set<String> usersPlayingARole) {
        execute(() -> {
          // get both the users playing currently a role in the community space and the users of
          // the members group associated with the community space
          if (usersPlayingARole.isEmpty()) {
            // if there is no users, no need of synchronization
            return;
          }
          var space = CommunitySpace.this.getSilverpeasSpace();
          var group = getMembersGroup();
          if (group == null) {
            group = createMembersGroup(space);
          }
          var members = Set.of(group.getUserIds());

          // ensure the users playing a role in the community space are also in the members group
          for (String userId : usersPlayingARole) {
            if (!members.contains(userId)) {
              administration.addUserInGroup(userId, group.getId());
            }
          }

          // ensure all users in the members group are playing a role in the community space
          for (String memberId : members) {
            if (!usersPlayingARole.contains(memberId)) {
              administration.removeUserFromGroup(memberId, group.getId());
            }
          }
        });
      }
    }
  }
}