/*
 * 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:
 * "http://www.silverpeas.org/docs/core/legal/floss_exception.html"
 *
 * 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.kmelia.service;

import org.apache.commons.io.FilenameUtils;
import org.silverpeas.components.kmelia.*;
import org.silverpeas.components.kmelia.model.*;
import org.silverpeas.components.kmelia.notification.*;
import org.silverpeas.core.ActionType;
import org.silverpeas.core.ResourceReference;
import org.silverpeas.core.admin.PaginationPage;
import org.silverpeas.core.admin.ProfiledObjectId;
import org.silverpeas.core.admin.service.AdminController;
import org.silverpeas.core.admin.service.OrganizationController;
import org.silverpeas.core.admin.service.RemovedSpaceAndComponentInstanceChecker;
import org.silverpeas.core.admin.user.model.ProfileInst;
import org.silverpeas.core.admin.user.model.SilverpeasRole;
import org.silverpeas.core.admin.user.model.User;
import org.silverpeas.core.annotation.Service;
import org.silverpeas.core.comment.service.CommentService;
import org.silverpeas.core.contribution.attachment.AttachmentException;
import org.silverpeas.core.contribution.attachment.model.*;
import org.silverpeas.core.contribution.attachment.notification.AttachmentRef;
import org.silverpeas.core.contribution.content.form.FormException;
import org.silverpeas.core.contribution.content.form.RecordSet;
import org.silverpeas.core.contribution.content.form.RecordTemplate;
import org.silverpeas.core.contribution.content.form.record.GenericRecordSet;
import org.silverpeas.core.contribution.content.wysiwyg.WysiwygException;
import org.silverpeas.core.contribution.content.wysiwyg.service.WysiwygController;
import org.silverpeas.core.contribution.model.ContributionIdentifier;
import org.silverpeas.core.contribution.publication.dao.DistributionTreeCriteria;
import org.silverpeas.core.contribution.publication.dao.PublicationCriteria;
import org.silverpeas.core.contribution.publication.model.*;
import org.silverpeas.core.contribution.publication.service.PublicationService;
import org.silverpeas.core.contribution.template.form.dao.ModelDAO;
import org.silverpeas.core.contribution.template.publication.PublicationTemplate;
import org.silverpeas.core.contribution.template.publication.PublicationTemplateException;
import org.silverpeas.core.contribution.template.publication.PublicationTemplateManager;
import org.silverpeas.core.i18n.I18NHelper;
import org.silverpeas.core.index.indexing.model.IndexManager;
import org.silverpeas.core.io.media.image.thumbnail.ThumbnailException;
import org.silverpeas.core.io.media.image.thumbnail.control.ThumbnailController;
import org.silverpeas.core.io.media.image.thumbnail.model.ThumbnailDetail;
import org.silverpeas.core.io.media.image.thumbnail.service.ThumbnailServiceProvider;
import org.silverpeas.core.node.coordinates.model.Coordinate;
import org.silverpeas.core.node.coordinates.model.CoordinatePK;
import org.silverpeas.core.node.coordinates.model.CoordinatePoint;
import org.silverpeas.core.node.coordinates.service.CoordinatesService;
import org.silverpeas.core.node.model.NodeDetail;
import org.silverpeas.core.node.model.NodePK;
import org.silverpeas.core.node.service.NodeService;
import org.silverpeas.core.notification.user.UserNotification;
import org.silverpeas.core.notification.user.builder.UserNotificationBuilder;
import org.silverpeas.core.notification.user.builder.helper.UserNotificationHelper;
import org.silverpeas.core.notification.user.client.constant.NotifAction;
import org.silverpeas.core.pdc.pdc.model.ClassifyPosition;
import org.silverpeas.core.pdc.pdc.model.PdcClassification;
import org.silverpeas.core.pdc.pdc.model.PdcException;
import org.silverpeas.core.pdc.pdc.service.PdcClassificationService;
import org.silverpeas.core.pdc.pdc.service.PdcManager;
import org.silverpeas.core.pdc.subscription.service.PdcSubscriptionManager;
import org.silverpeas.core.persistence.jdbc.DBUtil;
import org.silverpeas.core.personalorganizer.model.Attendee;
import org.silverpeas.core.personalorganizer.model.TodoDetail;
import org.silverpeas.core.personalorganizer.service.SilverpeasCalendar;
import org.silverpeas.core.process.annotation.SimulationActionProcess;
import org.silverpeas.core.reminder.Reminder;
import org.silverpeas.core.security.authorization.AccessControlContext;
import org.silverpeas.core.security.authorization.ComponentAccessControl;
import org.silverpeas.core.security.authorization.NodeAccessControl;
import org.silverpeas.core.security.authorization.PublicationAccessControl;
import org.silverpeas.core.silverstatistics.access.model.HistoryObjectDetail;
import org.silverpeas.core.silverstatistics.access.service.StatisticService;
import org.silverpeas.core.util.*;
import org.silverpeas.core.util.annotation.Action;
import org.silverpeas.core.util.annotation.SourcePK;
import org.silverpeas.core.util.annotation.TargetPK;
import org.silverpeas.core.util.file.FileRepositoryManager;
import org.silverpeas.core.util.file.FileUtil;
import org.silverpeas.kernel.annotation.NonNull;
import org.silverpeas.kernel.bundle.LocalizationBundle;
import org.silverpeas.kernel.bundle.ResourceLocator;
import org.silverpeas.kernel.bundle.SettingBundle;
import org.silverpeas.kernel.logging.SilverLogger;
import org.silverpeas.kernel.util.Pair;
import org.silverpeas.kernel.util.StringUtil;

import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.transaction.Transactional;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response.Status;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import static java.text.MessageFormat.format;
import static java.util.Collections.singletonList;
import static java.util.Comparator.comparing;
import static java.util.Optional.ofNullable;
import static java.util.function.Predicate.not;
import static java.util.stream.Collectors.toMap;
import static org.apache.commons.lang3.time.DurationFormatUtils.formatDurationHMS;
import static org.silverpeas.components.kmelia.model.KmeliaPublication.fromDetail;
import static org.silverpeas.components.kmelia.notification.KmeliaDelayedVisibilityUserNotificationReminder.KMELIA_DELAYED_VISIBILITY_USER_NOTIFICATION;
import static org.silverpeas.components.kmelia.service.KmeliaHelper.isToolbox;
import static org.silverpeas.components.kmelia.service.KmeliaOperationContext.OperationType.*;
import static org.silverpeas.components.kmelia.service.KmeliaServiceContext.*;
import static org.silverpeas.core.admin.component.model.ComponentInst.getComponentLocalId;
import static org.silverpeas.core.admin.service.OrganizationControllerProvider.getOrganisationController;
import static org.silverpeas.core.cache.service.CacheAccessorProvider.getThreadCacheAccessor;
import static org.silverpeas.core.contribution.attachment.AttachmentService.VERSION_MODE;
import static org.silverpeas.core.contribution.attachment.AttachmentServiceProvider.getAttachmentService;
import static org.silverpeas.core.contribution.content.wysiwyg.service.WysiwygController.deleteWysiwygAttachmentsOnly;
import static org.silverpeas.core.contribution.content.wysiwyg.service.WysiwygController.haveGotWysiwyg;
import static org.silverpeas.core.node.model.NodePK.UNDEFINED_NODE_ID;
import static org.silverpeas.core.notification.system.ResourceEvent.Type.MOVE;
import static org.silverpeas.core.persistence.Transaction.getTransaction;
import static org.silverpeas.core.security.authorization.AccessControlOperation.MODIFICATION;
import static org.silverpeas.kernel.util.StringUtil.*;

/**
 * This is the Kmelia Service controller of the MVC. It controls all the activities that happen in a
 * client session. It also provides mechanisms to access other services. Service which manage kmelia
 * and kmax application.
 * @author Nicolas Eysseric
 */
@Service
@Singleton
@Named("kmeliaService")
@Transactional(Transactional.TxType.SUPPORTS)
public class DefaultKmeliaService implements KmeliaService, KmeliaDeleter {

  private static final String MESSAGES_PATH = "org.silverpeas.kmelia.multilang.kmeliaBundle";
  private static final String SETTINGS_PATH = "org.silverpeas.kmelia.settings.kmeliaSettings";
  private static final SettingBundle settings = ResourceLocator.getSettingBundle(SETTINGS_PATH);
  private static final String UNKNOWN = "unknown";
  private static final String PUBLICATION = "Publication";
  private static final String USELESS = "useless";
  private static final String NODE_PREFIX = "Node_";
  private static final String ADMIN_ROLE = "admin";
  private static final String ALIASES_CACHE_KEY = "NEW_PUB_ALIASES";
  private static final Predicate<PublicationDetail> HAS_CLONE = k -> k.isValid() &&
      k.haveGotClone() && !k.isClone();
  @Inject
  private NodeService nodeService;
  @Inject
  private PublicationService publicationService;
  @Inject
  private StatisticService statisticService;
  @Inject
  private PdcManager pdcManager;
  @Inject
  private CoordinatesService coordinatesService;
  @Inject
  private CommentService commentService;
  @Inject
  private AdminController adminController;
  @Inject
  private SilverpeasCalendar calendar;
  @Inject
  private PdcClassificationService pdcClassificationService;
  @Inject
  private PdcSubscriptionManager pdcSubscriptionManager;
  @Inject
  private KmeliaContentManager kmeliaContentManager;

  private int getNbPublicationsOnRoot(String componentId) {
    String parameterValue =
        getOrganisationController().getComponentParameterValue(componentId, "nbPubliOnRoot");
    if (isDefined(parameterValue)) {
      return Integer.parseInt(parameterValue);
    } else {
      if (isToolbox(componentId)) {

        return 0;
      }
      // lecture du properties
      SettingBundle theSettings = getComponentSettings();
      return theSettings.getInteger("HomeNbPublications");
    }
  }

  private boolean isDraftModeUsed(String componentId) {
    return StringUtil.getBooleanValue(
        getOrganisationController().getComponentParameterValue(componentId, "draft"));
  }

  @Override
  public TopicDetail goTo(NodePK pk, String userId, boolean isTreeStructureUsed, String userProfile,
      boolean mustUserRightsBeChecked) {
    Collection<NodeDetail> newPath = new ArrayList<>();
    NodeDetail nodeDetail;

    // get the basic information (Header) of this topic
    try {
      nodeDetail = nodeService.getDetail(pk);
      if (mustUserRightsBeChecked) {
        if (!NodeAccessControl.get().isUserAuthorized(userId, nodeDetail)) {
          nodeDetail.setUserRole("noRights");
        }
        List<NodeDetail> availableChildren = getAllowedSubfolders(nodeDetail, userId);
        nodeDetail.setChildrenDetails(availableChildren);
      }
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
    // get publications
    List<KmeliaPublication> pubDetails =
        getAuthorizedPublicationsOfFolder(pk, userProfile, userId, isTreeStructureUsed);

    // get the path to this topic
    if (pk.isRoot()) {
      newPath.add(nodeDetail);
    } else {
      newPath = getPathFromAToZ(nodeDetail);
    }

    // set the currentTopic and return it
    return new TopicDetail(newPath, nodeDetail, pubDetails);
  }

  @Override
  public List<KmeliaPublication> getAuthorizedPublicationsOfFolder(NodePK pk, String userProfile,
      String userId, boolean isTreeStructureUsed) {
    final long start = System.currentTimeMillis();
    final Collection<PublicationDetail> pubDetails;
    // get the publications associated to this topic
    if (pk.isTrash()) {
      // Topic = Basket
      pubDetails = getPublicationsInBasket(pk, userProfile, userId);
    } else if (pk.isRoot()) {
      try {
        int nbPublisOnRoot = getNbPublicationsOnRoot(pk.getInstanceId());
        if (nbPublisOnRoot == 0 || !isTreeStructureUsed || isToolbox(pk.getInstanceId())) {
          pubDetails = publicationService.getDetailsByFatherPK(pk, "P.pubUpdateDate desc", false);
        } else {
          return getLatestAuthorizedPublications(pk.getInstanceId(), userId, nbPublisOnRoot);
        }
      } catch (Exception e) {
        throw new KmeliaRuntimeException(e);
      }
    } else {
      try {
        // get the publication details linked to this topic
        pubDetails =
            publicationService.getDetailsByFatherPK(pk, "P.pubUpdateDate DESC, P.pubId DESC",
                false);
      } catch (Exception e) {
        throw new KmeliaRuntimeException(e);
      }
    }
    final List<KmeliaPublication> result = filterPublications(
        asLocatedKmeliaPublication(pk, pubDetails), pk.getInstanceId(),
        SilverpeasRole.fromString(userProfile), userId);
    SilverLogger.getLogger(this)
        .debug(() -> format("getting {0} publications of folder {1} in {2}",
            result.size(), pk, formatDurationHMS(System.currentTimeMillis() - start)));
    return result;
  }

  @SuppressWarnings("unchecked")
  @Override
  public List<KmeliaPublication> getLatestAuthorizedPublications(String instanceId, String userId,
      int limit) {
    final long start = System.currentTimeMillis();
    final List<KmeliaPublication> result = getThreadCacheAccessor().getCache()
        .computeIfAbsent(
            "KmeliaService:getLatestAuthorizedPublications:" + instanceId + ":" + userId + ":" +
                limit, List.class, () -> {
              final List<PublicationDetail> pubDetails =
                  publicationService.getAuthorizedPublicationsForUserByCriteria(userId,
                      PublicationCriteria.excludingTrashNodeOnComponentInstanceIds(instanceId)
                          .excludingNodes(NodePK.ROOT_NODE_ID)
                          .ofStatus(PublicationDetail.VALID_STATUS)
                          .visibleAt(OffsetDateTime.now())
                          .takingAliasesIntoAccount()
                          .orderByDescendingBeginDate()
                          .limitTo(limit));
              return asKmeliaPublication(pubDetails);
            });
    int size = result == null ? 0 : result.size();
    SilverLogger.getLogger(this)
        .debug(() -> format("getting {0} latest authorized publications of instance {1} in {2}",
            size, instanceId, formatDurationHMS(System.currentTimeMillis() - start)));
    return result;
  }

  @Override
  public List<NodeDetail> getAllowedSubfolders(NodeDetail folder, String userId) {
    final List<NodeDetail> children = (List<NodeDetail>) folder.getChildrenDetails();
    final List<NodeDetail> availableChildren = new ArrayList<>();
    for (final NodeDetail child : children) {
      final NodePK childId = child.getNodePK();
      if (childId.isTrash() || childId.isUnclassed() || !child.haveRights()) {
        availableChildren.add(child);
      } else {
        addAccordingToRights(userId, availableChildren, child);
      }
    }
    return availableChildren;
  }

  private void addAccordingToRights(final String userId, final List<NodeDetail> availableChildren,
      final NodeDetail child) {
    final String rightsDependsOn = child.getRightsDependsOn();
    final boolean nodeAvailable = getOrganisationController().isObjectAvailableToUser(
        ProfiledObjectId.fromNode(rightsDependsOn), child.getNodePK().getInstanceId(), userId);
    if (nodeAvailable) {
      availableChildren.add(child);
    } else { // check if at least one descendant is available
      Iterator<NodeDetail> descendants = nodeService.getDescendantDetails(child).iterator();
      addDescendantIfAvailable(userId, availableChildren, child, rightsDependsOn, descendants);
    }
  }

  private void addDescendantIfAvailable(final String userId,
      final List<NodeDetail> availableChildren, final NodeDetail child,
      final String rightsDependsOn, final Iterator<NodeDetail> descendants) {
    boolean childAllowed = false;
    while (!childAllowed && descendants.hasNext()) {
      NodeDetail descendant = descendants.next();
      if (!descendant.getRightsDependsOn().equals(rightsDependsOn) &&
          getOrganisationController().isObjectAvailableToUser(
              ProfiledObjectId.fromNode(descendant.getRightsDependsOn()),
              descendant.getNodePK().getInstanceId(), userId)) {
        // different rights of father check if it is available
        childAllowed = true;
        if (!availableChildren.contains(child)) {
          availableChildren.add(child);
        }
      }
    }
  }

  private Collection<NodeDetail> getPathFromAToZ(NodeDetail nd) {
    Collection<NodeDetail> newPath = new ArrayList<>();
    try {
      List<NodeDetail> pathInReverse = nodeService.getPath(nd.getNodePK());
      // reverse the path from root to leaf
      for (int i = pathInReverse.size() - 1; i >= 0; i--) {
        newPath.add(pathInReverse.get(i));
      }
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
    return newPath;
  }

  /**
   * Add a subtopic to a topic - If a subtopic of same name already exists a NodePK with id=-1 is
   * returned else the new topic NodePK
   * @param fatherPK the topic Id of the future father
   * @param subTopic the NodeDetail of the new sub topic
   * @return If a subtopic of same name already exists a NodePK with id=-1 is returned else the new
   * topic NodePK
   * @see NodeDetail
   * @see NodePK
   */
  @Override
  public NodeDetail addToTopic(NodePK fatherPK, NodeDetail subTopic) {
    try {
      NodeDetail fatherDetail = nodeService.getHeader(fatherPK);
      return nodeService.createNode(subTopic, fatherDetail);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  @Override
  public NodeDetail addSubTopic(NodePK fatherPK, NodeDetail subTopic, String alertType) {
    // Construction de la date de création (date courante)
    subTopic.setCreationDate(new Date());
    // Web visibility parameter. The topic is by default invisible.
    subTopic.setStatus("Invisible");
    // add new topic to current topic
    NodeDetail addedTopic = addToTopic(fatherPK, subTopic);
    // Creation alert
    subTopic.setNodePK(addedTopic.getNodePK());
    subTopic.setFatherPK(fatherPK);
    topicCreationAlert(subTopic, NotifAction.CREATE, alertType);
    return addedTopic;
  }

  /**
   * Alert all users, only publishers or nobody of the topic creation or update
   * @param alertType alertType = "All"|"Publisher"|"None"
   * @see NodePK
   * @since 1.0
   */
  private void topicCreationAlert(final NodeDetail node, NotifAction action,
      final String alertType) {
    UserNotificationHelper.buildAndSend(new KmeliaTopicUserNotification(node, action, alertType));
  }

  @Override
  public NodeDetail updateTopic(NodeDetail topic, String alertType) {
    // Order of the node must be unchanged
    NodeDetail oldNode = nodeService.getHeader(topic.getNodePK());
    int order = oldNode.getOrder();
    topic.setOrder(order);
    nodeService.setDetail(topic);

    // manage operations relative to folder rights
    if (isRightsOnTopicsEnabled(topic.getNodePK().getInstanceId())) {
      updateNode(topic, oldNode);
    }

    // Update Alert
    topic.setFatherPK(oldNode.getFatherPK());
    topicCreationAlert(topic, NotifAction.UPDATE, alertType);
    return topic;
  }

  private void updateNode(final NodeDetail newNode, final NodeDetail oldNode) {
    if (!oldNode.getRightsDependsOn().equals(newNode.getRightsDependsOn())) {
      // rights dependency have changed
      if (!newNode.haveRights()) {

        NodeDetail father = nodeService.getHeader(oldNode.getFatherPK());
        newNode.setRightsDependsOn(father.getRightsDependsOn());

        // Topic profiles must be removed
        List<ProfileInst> profiles = adminController.getProfilesByObject(
            ProfiledObjectId.fromNode(newNode.getNodePK().getId()),
            newNode.getNodePK().getInstanceId());
        deleteProfiles(profiles);
      } else {
        newNode.setRightsDependsOnMe();
      }
      nodeService.updateRightsDependency(newNode);
    }
  }

  private void deleteProfiles(final List<ProfileInst> profiles) {
    for (ProfileInst profile : profiles) {
      adminController.deleteProfileInst(profile.getId());
    }
  }

  @Override
  public NodeDetail getSubTopicDetail(NodePK pk) {
    NodeDetail subTopic;
    // get the basic information (Header) of this topic
    try {
      subTopic = nodeService.getDetail(pk);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
    return subTopic;
  }

  /**
   * Delete a topic and all descendants. Delete all links between descendants and publications. This
   * publications will be visible in the Declassified zone. Delete All subscriptions and favorites
   * on this topics and all descendants
   *
   * @param pkToDelete the id of the topic to delete
   * @since 1.0
   */
  @Override
  @Transactional(Transactional.TxType.REQUIRED)
  public void deleteTopic(NodePK pkToDelete) {
    try {
      // get all nodes which will be deleted
      final Collection<NodePK> nodesToDelete = nodeService.getDescendantPKs(pkToDelete);
      nodesToDelete.add(pkToDelete);
      for (final NodePK oneNodeToDelete : nodesToDelete) {
        // get pubs linked to current node (includes alias)
        final Collection<PublicationDetail> pubsToCheck =
            publicationService.getDetailsByFatherPK(oneNodeToDelete);
        // check each pub contained in current node
        for (PublicationDetail onePubToCheck : pubsToCheck) {
          final KmeliaPublication kmeliaPub = fromDetail(onePubToCheck, oneNodeToDelete);
          if (!kmeliaPub.isAlias()) {
            // delete definitively the publication
            deletePublication(kmeliaPub.getPk());
          } else {
            // remove only the alias
            final Collection<Location> aliases = singletonList(kmeliaPub.getLocation());
            publicationService.removeAliases(kmeliaPub.getPk(), aliases);
          }
        }
      }

      // Delete the topic
      nodeService.deleteNode(pkToDelete);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  @Override
  public void changeTopicStatus(String newStatus, NodePK nodePK, boolean recursiveChanges) {
    try {
      if (!recursiveChanges) {
        NodeDetail nodeDetail = nodeService.getHeader(nodePK);
        changeTopicStatus(newStatus, nodeDetail);
      } else {
        List<NodeDetail> subTree = nodeService.getSubTree(nodePK);
        for (NodeDetail aSubTree : subTree) {
          changeTopicStatus(newStatus, aSubTree);
        }
      }
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  @Override
  public void sortSubTopics(NodePK fatherPK, boolean recursive, String[] criteria) {
    List<NodeDetail> subTopics;
    try {
      subTopics = (List<NodeDetail>) nodeService.getChildrenDetails(fatherPK);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }

    if (subTopics != null && !subTopics.isEmpty()) {
      subTopics.sort(new TopicComparator(criteria));
      // for each node, change the order and store it
      for (int i = 0; i < subTopics.size(); i++) {
        NodeDetail nodeDetail = subTopics.get(i);
        try {
          nodeDetail.setOrder(i);
          nodeService.setDetail(nodeDetail);
        } catch (Exception e) {
          throw new KmeliaRuntimeException(e);
        }
        if (recursive) {
          sortSubTopics(nodeDetail.getNodePK(), true, criteria);
        }
      }
    }
  }

  private void changeTopicStatus(String newStatus, NodeDetail topic) {
    try {
      topic.setStatus(newStatus);
      nodeService.setDetail(topic);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  @Override
  public List<NodeDetail> getTreeview(NodePK nodePK, String profile, boolean coWritingEnable,
      boolean draftVisibleWithCoWriting, String userId, boolean displayNb,
      boolean isRightsOnTopicsUsed) {
    final long start = System.currentTimeMillis();

    String instanceId = nodePK.getInstanceId();
    List<NodeDetail> allowedTree = nodeService.getSubTree(nodePK);

    if (profile == null) {
      profile = getProfile(userId, nodePK);
    }

    KmeliaUserTreeViewFilter.from(userId, instanceId, nodePK, profile, isRightsOnTopicsUsed)
        .setBestUserRoleAndFilter(allowedTree);

    if (displayNb) {
      buildTreeView(nodePK, profile, coWritingEnable, draftVisibleWithCoWriting, userId,
          allowedTree);
    }

    SilverLogger.getLogger(this)
        .debug(() -> format("getting {0} nodes from folder {1} in {2}",
            allowedTree.size(), nodePK, formatDurationHMS(System.currentTimeMillis() - start)));

    return allowedTree;
  }

  private void buildTreeView(final NodePK nodePK, final String profile,
      final boolean coWritingEnable, final boolean draftVisibleWithCoWriting, final String userId,
      final List<NodeDetail> allowedTree) {
    boolean checkVisibility = false;
    StringBuilder statusSubQuery = new StringBuilder();
    if (profile == null || profile.equals("user")) {
      checkVisibility = true;
      statusSubQuery.append(" AND sb_publication_publi.pubStatus = 'Valid' ");
    } else if (profile.equals("writer")) {
      statusSubQuery.append(" AND (");
      if (coWritingEnable && draftVisibleWithCoWriting) {
        statusSubQuery.append("sb_publication_publi.pubStatus = 'Valid' OR ")
            .append("sb_publication_publi.pubStatus = 'Draft' OR ")
            .append("sb_publication_publi.pubStatus = 'Unvalidate' ");
      } else {
        checkVisibility = true;
        statusSubQuery.append("sb_publication_publi.pubStatus = 'Valid' OR ")
            .append("(sb_publication_publi.pubStatus = 'Draft' AND ")
            .append("sb_publication_publi.pubUpdaterId = '")
            .append(userId)
            .append("') OR (sb_publication_publi.pubStatus = 'Unvalidate' AND ")
            .append("sb_publication_publi.pubUpdaterId = '")
            .append(userId)
            .append("') ");
      }
      statusSubQuery.append("OR (sb_publication_publi.pubStatus = 'ToValidate' ")
          .append("AND sb_publication_publi.pubUpdaterId = '")
          .append(userId)
          .append("') ");
      statusSubQuery.append("OR sb_publication_publi.pubUpdaterId = '").append(userId).append("')");
    } else {
      statusSubQuery.append(" AND (");
      if (coWritingEnable && draftVisibleWithCoWriting) {
        statusSubQuery.append("sb_publication_publi.pubStatus IN ('Valid','ToValidate','Draft') ");
      } else {
        if (profile.equals("publisher")) {
          checkVisibility = true;
        }
        statusSubQuery.append("sb_publication_publi.pubStatus IN ('Valid','ToValidate') OR " +
                "(sb_publication_publi" +
                ".pubStatus = 'Draft' AND sb_publication_publi.pubUpdaterId = '")
            .append(userId)
            .append("') ");
      }
      statusSubQuery.append("OR sb_publication_publi.pubUpdaterId = '").append(userId).append("')");
    }
    final DistributionTreeCriteria criteria = DistributionTreeCriteria
        .onInstanceId(nodePK.getInstanceId())
        .withVisibilityCheck(checkVisibility)
        .withManualStatusFilter(statusSubQuery.toString());
    final Map<String, Integer> numbers = publicationService.getDistributionTree(criteria);

    // set right number of publications in basket
    NodePK trashPk = new NodePK(NodePK.BIN_NODE_ID, nodePK.getInstanceId());
    int nbPubsInTrash = getPublicationsInBasket(trashPk, profile, userId).size();
    numbers.put(NodePK.BIN_NODE_ID, nbPubsInTrash);

    decorateWithNumberOfPublications(allowedTree, numbers);
  }

  private void decorateWithNumberOfPublications(List<NodeDetail> nodes,
      Map<String, Integer> numbers) {
    for (NodeDetail node : nodes) {
      decorateWithNumberOfPublications(node, numbers);
    }
  }

  private int decorateWithNumberOfPublications(NodeDetail node, Map<String, Integer> numbers) {
    Integer nb = numbers.get(node.getNodePK().getId());
    for (NodeDetail child : node.getChildrenDetails()) {
      nb += decorateWithNumberOfPublications(child, numbers);
    }
    node.setNbObjects(nb);
    return nb;
  }

  private Collection<PublicationDetail> getPublicationsInBasket(NodePK pk, String userProfile,
      String userId) {
    String currentUserId = userId;
    try {
      // Give the publications associated to basket topic and visibility period expired
      if (SilverpeasRole.ADMIN.isInRole(userProfile)) {// Admin can see all Publis in the basket.
        currentUserId = null;
      }
      return publicationService.getDetailsByFatherPK(pk, null, false, currentUserId);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  private List<KmeliaPublication> asLocatedKmeliaPublication(NodePK fatherPK,
      Collection<PublicationDetail> pubDetails) {
    final Collection<String> pubIds = pubDetails.stream()
        .map(PublicationDetail::getId)
        .collect(Collectors.toSet());
    final Map<String, List<Location>> locationsByPublication =
        publicationService.getAllLocationsByPublicationIds(pubIds);
    return pubDetails.stream()
        .map(p -> KmeliaPublication.fromDetail(p, fatherPK, locationsByPublication))
        .collect(Collectors.toList());
  }

  private List<KmeliaPublication> asKmeliaPublication(Collection<PublicationDetail> pubDetails) {
    return pubDetails.stream().map(KmeliaPublication::fromDetail).collect(Collectors.toList());
  }

  /**
   * Return the detail of a publication (only the Header)
   * @param pubPK the id of the publication
   * @return a PublicationDetail
   * @see org.silverpeas.core.contribution.publication.model.PublicationDetail
   * @since 1.0
   */
  @Override
  public PublicationDetail getPublicationDetail(PublicationPK pubPK) {
    try {
      return publicationService.getDetail(pubPK);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  /**
   * Return list of all path to this publication - it's a Collection of NodeDetail collection
   * @param pubPK the id of the publication
   * @return a Collection of NodeDetail collection
   * @see NodeDetail
   * @since 1.0
   */
  @Override
  public Collection<Collection<NodeDetail>> getPathList(PublicationPK pubPK) {
    Collection<NodePK> fatherPKs;
    try {
      // get PK of all nodes which contain this publication
      fatherPKs = publicationService.getAllFatherPKInSamePublicationComponentInstance(pubPK);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
    try {
      List<Collection<NodeDetail>> pathList = new ArrayList<>();
      if (fatherPKs != null) {
        // For each topic, get the path to it
        for (NodePK pk : fatherPKs) {
          Collection<NodeDetail> path = nodeService.getPath(pk);
          // add this path
          pathList.add(path);
        }
      }
      return pathList;
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  /**
   * Create a new Publication (only the header - parameters) to the current Topic
   * @param pubDetail a PublicationDetail
   * @return the id of the new publication
   * @see org.silverpeas.core.contribution.publication.model.PublicationDetail
   * @since 1.0
   */
  @Override
  @Transactional(Transactional.TxType.REQUIRED)
  public String createPublicationIntoTopic(PublicationDetail pubDetail, NodePK fatherPK) {
    PdcClassification predefinedClassification =
        pdcClassificationService.findAPreDefinedClassification(fatherPK.getId(),
            fatherPK.getInstanceId());
    return createPublicationIntoTopic(pubDetail, fatherPK, predefinedClassification);
  }

  @Override
  @Transactional(Transactional.TxType.REQUIRED)
  public String createPublicationIntoTopic(PublicationDetail pubDetail, NodePK fatherPK,
      PdcClassification classification) {
    final String pubId;
    KmeliaOperationContext.about(CREATION);
    try {
      pubId = createPublicationIntoTopicWithoutNotifications(pubDetail, fatherPK, classification);

      // creates todos for publishers
      createTodosForPublication(pubDetail, true);

      // alert supervisors
      sendAlertToSupervisors(fatherPK, pubDetail);

      // alert subscribers
      sendSubscriptionsNotification(pubDetail, NotifAction.CREATE, false);

    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
    return pubId;
  }

  private String createPublicationIntoTopicWithoutNotifications(PublicationDetail pubDetail,
      NodePK fatherPK, PdcClassification classification) {
    PublicationPK pubPK;
    try {
      PublicationDetail detail = changePublicationStatusOnCreation(pubDetail, fatherPK);
      if (isInTrash(fatherPK)) {
        detail.setRemovalStatus(detail.getCreationDate(), detail.getCreatorId());
      }
      // create the publication
      pubPK = publicationService.createPublication(detail);
      detail.getPK().setId(pubPK.getId());
      // register the new publication as a new content to content manager
      createSilverContent(detail, detail.getCreatorId());
      // add this publication to the current topic
      addPublicationToTopicWithoutNotifications(pubPK, fatherPK, true);
      // classify the publication on the PdC if its classification is defined
      // subscribers are notified later (only if publication is valid)
      classification.classifyContent(detail, false);

      createdIntoRequestContext(detail);

    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
    return pubPK.getId();
  }

  private String getProfile(String userId, NodePK nodePK) {
    OrganizationController orgCtrl = getOrganisationController();
    if (isRightsOnTopicsEnabled(nodePK.getInstanceId())) {
      NodeDetail topic = nodeService.getHeader(nodePK);
      if (topic.haveRights()) {
        ProfiledObjectId nodeId = ProfiledObjectId.fromNode(topic.getRightsDependsOn());
        return KmeliaHelper.getProfile(
            orgCtrl.getUserProfiles(userId, nodePK.getInstanceId(), nodeId));
      }
    }
    return KmeliaHelper.getProfile(getUserRoles(nodePK.getInstanceId(), userId));
  }

  private String getProfileOnPublication(String userId, PublicationPK pubPK) {
    final NodePK fatherPK = getPublicationFatherPK(pubPK);
    return getProfileForDirectNodeOfPublication(userId, pubPK, fatherPK);
  }

  private String getProfileForDirectNodeOfPublication(final String userId,
      final PublicationPK pubPK, final NodePK fatherPK) {
    final String profile;
    if (fatherPK != null) {
      profile = getProfile(userId, fatherPK);
    } else {
      // peculiar case in which the publication isn't in any node: this situation occurs if the
      // publication is definitely deleted or orphaned. In that case, we take the profile of the
      // user in the concerned kmelia instance. This shouldn't occur!
      SilverLogger.getLogger(this).warn("The publication {0} is orphaned!", pubPK);
      //noinspection ConstantConditions
      profile = SilverpeasRole.getHighestFrom(SilverpeasRole.fromStrings(
          getOrganisationController().getUserProfiles(userId, pubPK.getInstanceId()))).getName();

    }
    return profile;
  }

  private PublicationDetail changePublicationStatusOnCreation(PublicationDetail pubDetail,
      NodePK nodePK) {
    String status = pubDetail.getStatus();
    if (!isDefined(status)) {
      status = PublicationDetail.TO_VALIDATE_STATUS;

      boolean draftModeUsed = isDraftModeUsed(pubDetail.getPK().getInstanceId());

      if (draftModeUsed) {
        status = PublicationDetail.DRAFT_STATUS;
      } else {
        String profile = getProfile(pubDetail.getCreatorId(), nodePK);
        if (SilverpeasRole.PUBLISHER.isInRole(profile) || SilverpeasRole.ADMIN.isInRole(profile)) {
          status = PublicationDetail.VALID_STATUS;
        }
      }
    }
    pubDetail.setStatus(status);
    KmeliaHelper.checkIndex(pubDetail);
    return pubDetail;
  }

  private Pair<Boolean, String> changePublicationStatusOnMove(PublicationDetail pub, NodePK to,
      final String currentUserId) {
    String oldStatus = pub.getStatus();
    String status = pub.getStatus();
    if (!status.equals(PublicationDetail.DRAFT_STATUS)) {
      status = PublicationDetail.TO_VALIDATE_STATUS;
      String profile = getProfile(currentUserId, to);
      if (SilverpeasRole.PUBLISHER.isInRole(profile) || SilverpeasRole.ADMIN.isInRole(profile)) {
        status = PublicationDetail.VALID_STATUS;
      }
    }
    pub.setStatus(status);
    KmeliaHelper.checkIndex(pub);
    return Pair.of(!oldStatus.equals(status), status);
  }

  private boolean changePublicationStatusOnUpdate(PublicationDetail pubDetail) {
    final String previousStatus = pubDetail.getStatus();
    final String newStatus;
    final NodePK father = getPublicationFatherPK(pubDetail.getPK());
    if (pubDetail.isStatusMustBeChecked() && !pubDetail.isDraft() && !pubDetail.isClone()) {
      newStatus = setPublicationStatus(pubDetail, previousStatus, father);
    } else {
      newStatus = previousStatus;
    }

    KmeliaHelper.checkIndex(pubDetail);

    if (father == null || father.isTrash()) {
      // the publication is in the trash
      pubDetail.setIndexOperation(IndexManager.NONE);
    }
    return !previousStatus.equalsIgnoreCase(newStatus);
  }

  private String setPublicationStatus(final PublicationDetail pubDetail, final String newStatus,
      final NodePK father) {
    final String profile =
        getProfileForDirectNodeOfPublication(pubDetail.getUpdaterId(), pubDetail.getPK(), father);
    final String status;
    if (SilverpeasRole.WRITER.isInRole(profile)) {
      status = PublicationDetail.TO_VALIDATE_STATUS;
    } else if (pubDetail.isRefused() &&
        (SilverpeasRole.ADMIN.isInRole(profile) || SilverpeasRole.PUBLISHER.isInRole(profile))) {
      status = PublicationDetail.VALID_STATUS;
    } else {
      status = newStatus;
    }
    pubDetail.setStatus(status);
    return status;
  }

  /**
   * Update a publication (only the header - parameters)
   * @param pubDetail a PublicationDetail
   * @see org.silverpeas.core.contribution.publication.model.PublicationDetail
   * @since 1.0
   */
  @Override
  public void updatePublication(PublicationDetail pubDetail) {
    updatePublication(pubDetail, KmeliaHelper.PUBLICATION_HEADER, false);
  }

  @Override
  public void updatePublication(final PublicationDetail pubDetail,
      final PdcClassification classification) {
    updatePublication(pubDetail, classification, KmeliaHelper.PUBLICATION_HEADER, false);
  }

  @Override
  public void updatePublication(PublicationDetail pubDetail, boolean forceUpdateDate) {
    updatePublication(pubDetail, KmeliaHelper.PUBLICATION_HEADER, forceUpdateDate);
  }

  private void updatePublication(PublicationDetail pubDetail, int updateScope,
      boolean forceUpdateDate) {
    updatePublication(pubDetail, null, updateScope, forceUpdateDate);
  }

  private void updatePublication(PublicationDetail pubDetail, PdcClassification classification,
      int updateScope, boolean forceUpdateDate) {
    KmeliaOperationContext.about(UPDATE);
    try {
      // if pubDetail is a clone
      boolean isClone = pubDetail.isClone();
      PublicationDetail old = getPublicationDetail(pubDetail.getPK());

      // prevents to lose some data
      if (StringUtil.isDefined(old.getTargetValidatorId()) &&
          !StringUtil.isDefined(pubDetail.getTargetValidatorId())) {
        pubDetail.setTargetValidatorId(old.getTargetValidatorId());
      }
      final boolean isPublicationInBasket = isPublicationInBasket(pubDetail.getPK());
      if (isClone) {
        // update only updateDate
        publicationService.setDetail(pubDetail, forceUpdateDate);
        performValidatorChanges(old, pubDetail);
      } else {
        boolean statusChanged = changePublicationStatusOnUpdate(pubDetail);
        publicationService.setDetail(pubDetail, forceUpdateDate);

        if (!isPublicationInBasket) {
          updatePublicationContent(pubDetail, updateScope, old, statusChanged);
        }
      }

      if (classification != null) {
        // classify the publication on the PdC if any
        // subscribers are notified later (only if publication is valid)
        classification.classifyContentOrClearClassificationIfEmpty(pubDetail, false);
      }

      // Sending a subscription notification if the publication updated comes not from the
      // basket, has not been created or already updated from the same request
      if (!isPublicationInBasket && !hasPublicationBeenCreatedFromRequestContext(pubDetail) &&
          !hasPublicationBeenUpdatedFromRequestContext(pubDetail)) {
        sendSubscriptionsNotification(pubDetail, NotifAction.UPDATE, false);
      }


      updatedIntoRequestContext(pubDetail);

    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  private void updatePublicationContent(final PublicationDetail pubDetail, final int updateScope,
      final PublicationDetail old, final boolean statusChanged) {
    if (statusChanged) {
      // creates todos for publishers
      this.createTodosForPublication(pubDetail, false);
    } else {
      performValidatorChanges(old, pubDetail);
    }

    updateSilverContentVisibility(pubDetail);

    // la publication a été modifié par un superviseur
    // le créateur de la publi doit être averti
    String profile = KmeliaHelper.getProfile(
        getUserRoles(pubDetail.getPK().getInstanceId(), pubDetail.getUpdaterId()));
    if ("supervisor".equals(profile)) {
      sendModificationAlert(updateScope, pubDetail.getPK());
    }

    boolean visibilityPeriodUpdated = isVisibilityPeriodUpdated(pubDetail, old);

    if (statusChanged || visibilityPeriodUpdated) {
      if (KmeliaHelper.isIndexable(pubDetail)) {
        indexExternalElementsOfPublication(pubDetail);
      } else {
        unIndexExternalElementsOfPublication(pubDetail.getPK());
      }
    }
  }

  /**
   * Performs the treatments associated to changes about set of validators linked to a publication.
   * @param previousPublication the publication data (or clone data) before changes.
   * @param currentPublication the publication data (or clone data if previousPublication is a
   * clone) containing the changes.
   */
  private void performValidatorChanges(final PublicationDetail previousPublication,
      final PublicationDetail currentPublication) {

    // The publication (or the clone) must be into "ToValidate" state, and validator identifiers
    // must exist.
    if (currentPublication.isValidationRequired() &&
        currentPublication.getTargetValidatorIds() != null) {

      // Getting validator identifiers from previous and current data.
      List<String> oldValidatorIds = Arrays.asList(previousPublication.getTargetValidatorIds());
      List<String> newValidatorIds = Arrays.asList(currentPublication.getTargetValidatorIds());

      // Computing identifiers of removed validators, and the ones of added validators.
      List<String> toRemoveToDo = new ArrayList<>(oldValidatorIds);
      toRemoveToDo.removeAll(newValidatorIds);
      ValidatorsList toAlert = new ValidatorsList(getActiveValidatorIds(currentPublication));
      toAlert.removeAll(oldValidatorIds);

      // Performing the actions.
      removeTodoForPublication(currentPublication.getPK(), toRemoveToDo);
      addTodoAndSendNotificationToValidators(currentPublication, toAlert);
    }
  }

  private boolean isVisibilityPeriodUpdated(PublicationDetail pubDetail, PublicationDetail old) {
    boolean beginVisibilityPeriodUpdated = (
        (pubDetail.getBeginDate() != null && old.getBeginDate() == null) ||
            (pubDetail.getBeginDate() == null && old.getBeginDate() != null) ||
            (pubDetail.getBeginDate() != null && old.getBeginDate() != null &&
                !pubDetail.getBeginDate().equals(old.getBeginDate())));
    boolean endVisibilityPeriodUpdated = (
        (pubDetail.getEndDate() != null && old.getEndDate() == null) ||
            (pubDetail.getEndDate() == null && old.getEndDate() != null) ||
            (pubDetail.getEndDate() != null && old.getEndDate() != null &&
                !pubDetail.getEndDate().equals(old.getEndDate())));
    return beginVisibilityPeriodUpdated || endVisibilityPeriodUpdated;
  }

  @Transactional
  @SimulationActionProcess(elementLister = KmeliaPublicationSimulationElementLister.class)
  @Action(ActionType.MOVE)
  @Override
  public void movePublication(@SourcePK PublicationPK pubPK, @TargetPK NodePK to,
      KmeliaPasteDetail pasteContext) {
    final NodePK fromNode = pasteContext.getFromPK();
    final KmeliaPublication publication = KmeliaPublication.withPK(pubPK, fromNode);
    if (publication.isAlias()) {
      final Location newLocation = new Location(to.getId(), to.getInstanceId());
      newLocation.setAsAlias(pasteContext.getUserId());
      publicationService.removeAliases(pubPK, singletonList(publication.getLocation()));
      publicationService.addAliases(pubPK, singletonList(newLocation));
    } else if (fromNode.getInstanceId().equals(to.getInstanceId())) {
      movePublicationInSameApplication(publication.getDetail(), to, pasteContext);
    } else {
      movePublicationInAnotherApplication(publication.getDetail(), to, pasteContext);
    }
  }

  @Transactional
  @SimulationActionProcess(elementLister = KmeliaPublicationSimulationElementLister.class)
  @Action(ActionType.MOVE)
  @Override
  public void movePublicationInSameApplication(@SourcePK PublicationPK pubPK, @TargetPK NodePK from,
      KmeliaPasteDetail pasteContext) {
    PublicationDetail pub = getPublicationDetail(pubPK);
    String userId = pasteContext.getUserId();
    NodePK to = pasteContext.getToPK();

    // check if user can cut publication from source folder
    String profile = getUserTopicProfile(from, userId);
    @SuppressWarnings("removal") boolean cutAllowed = KmeliaPublicationHelper.isCanBeCut(
        from.getComponentName(), userId, profile, pub.getCreator());

    // check if user can paste publication into target folder
    String profileInTarget = getUserTopicProfile(to, userId);
    boolean pasteAllowed = KmeliaPublicationHelper.isCreationAllowed(to, profileInTarget);

    if (cutAllowed && pasteAllowed) {
      movePublicationInSameApplication(pub, to, pasteContext);
    }
  }

  private void movePublicationInSameApplication(PublicationDetail pub, NodePK to,
      KmeliaPasteDetail pasteContext) {
    if (to.isTrash()) {
      sendPublicationInBasket(pub.getPK());
    } else {
      boolean isInTrash = isInTrash(to);
      // update parent
      publicationService.movePublication(pub.getPK(), to, false);
      if (pub.isRemoved() && !isInTrash) {
        publicationService.restorePublication(pub.getPK());
      } else if (isInTrash) {
        publicationService.removePublication(pub.getPK());
      }
      pub.setTargetValidatorId(pasteContext.getTargetValidatorIds());
      processPublicationAfterMove(pub, to, pasteContext.getUserId());
    }
  }

  /**
   * Move a publication to another component. Moving in this order : <ul>
   * <li>moving the metadata</li>
   * <li>moving the thumbnail</li>
   * <li>moving the content</li>
   * <li>moving the wysiwyg</li>
   * <li>moving the images linked to the wysiwyg</li>
   * <li>moving the xml form content (files and images)</li>
   * <li>moving attachments</li>
   * <li>moving the pdc poistion</li>
   * <li>moving the statistics</li>
   * </ul>
   */
  private void movePublicationInAnotherApplication(PublicationDetail pub, NodePK to,
      KmeliaPasteDetail pasteContext) {
    try {
      ResourceReference fromResourceReference = new ResourceReference(pub.getPK());
      String fromComponentId = pub.getInstanceId();
      ResourceReference toPubliResourceReference =
          new ResourceReference(pub.getId(), to.getInstanceId());

      // remove index relative to publication
      unIndexExternalElementsOfPublication(pub.getPK());

      // move thumbnail
      ThumbnailController.moveThumbnail(fromResourceReference, toPubliResourceReference);

      moveAdditionalFiles(pub, fromResourceReference, toPubliResourceReference);

      // change images path in wysiwyg
      WysiwygController.wysiwygPlaceHaveChanged(fromResourceReference.getInstanceId(),
          pub.getPK().getId(), to.getInstanceId(), pub.getPK().getId());

      // move regular files
      List<SimpleDocument> docs = getAttachmentService().listDocumentsByForeignKeyAndType(
          fromResourceReference, DocumentType.attachment, null);
      for (SimpleDocument doc : docs) {
        getAttachmentService().moveDocument(doc, toPubliResourceReference);
      }

      // move form content
      String infoId = pub.getInfoId();
      if (infoId != null && !"0".equals(infoId)) {
        // register content to component
        PublicationTemplateManager templateManager = PublicationTemplateManager.getInstance();
        GenericRecordSet toRecordSet = templateManager.addDynamicPublicationTemplate(
            to.getInstanceId() + ":" + pub.getInfoId(), pub.getInfoId() + ".xml");
        RecordTemplate toRecordTemplate = toRecordSet.getRecordTemplate();

        // get xmlContent to move
        PublicationTemplate pubTemplateFrom =
            templateManager.getPublicationTemplate(fromComponentId + ":" + pub.getInfoId());

        RecordSet set = pubTemplateFrom.getRecordSet();
        set.move(fromResourceReference, toPubliResourceReference, toRecordTemplate);
      }

      // move comments
      getCommentService().moveComments(PublicationDetail.getResourceType(), fromResourceReference,
          toPubliResourceReference);

      // move pdc positions
      // Careful! positions must be moved according to taxonomy restrictions of target application
      int fromSilverObjectId = getSilverObjectId(pub.getPK());
      // get positions of cutted publication
      List<ClassifyPosition> positions = pdcManager.getPositions(fromSilverObjectId,
          fromComponentId);

      // delete taxonomy data relative to moved publication
      deleteSilverContent(pub.getPK());

      // move statistics
      statisticService.moveStat(toPubliResourceReference, 1, PUBLICATION);

      // move publication itself
      publicationService.movePublication(pub.getPK(), to, false);
      pub.getPK().setComponentName(to.getInstanceId());

      pub.setTargetValidatorId(pasteContext.getTargetValidatorIds());

      processPublicationAfterMove(pub, to, pasteContext.getUserId());

      // index moved publication
      if (KmeliaHelper.isIndexable(pub)) {
        indexPublication(pub);
      }

      // reference pasted publication on taxonomy service
      int toSilverObjectId = getSilverObjectId(pub.getPK());
      // add original positions to pasted publication
      pdcManager.addPositions(positions, toSilverObjectId, to.getInstanceId());

      if (to.isTrash()) {
        sendPublicationInBasket(pub.getPK());
      } else if (isInTrash(to)) {
        publicationService.removePublication(pub.getPK());
      } else if (pub.isRemoved()) {
        publicationService.restorePublication(pub.getPK());
      }
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  private void moveAdditionalFiles(final @SourcePK PublicationDetail pub,
      final ResourceReference fromResourceReference,
      final ResourceReference toPubliResourceReference) {
    try {
      // move additional files
      List<SimpleDocument> documents =
          getAttachmentService().listDocumentsByForeignKeyAndType(fromResourceReference,
              DocumentType.image, null);
      documents.addAll(
          getAttachmentService().listDocumentsByForeignKeyAndType(fromResourceReference,
              DocumentType.wysiwyg, null));
      for (SimpleDocument doc : documents) {
        getAttachmentService().moveDocument(doc, toPubliResourceReference);
      }
    } catch (AttachmentException e) {
      SilverLogger.getLogger(this)
          .error("Cannot move attachments of publication {0}", new String[]{pub.getPK().getId()},
              e);
    }
  }

  private void processPublicationAfterMove(PublicationDetail pub, NodePK to, String currentUserId) {
    // status must be checked according to topic rights and last modifier (current user)
    final Pair<Boolean, String> statusChanges = changePublicationStatusOnMove(pub, to,
        currentUserId);
    final boolean statusChanged = statusChanges.getFirst();
    // update publication
    if (!statusChanged) {
      pub.setUpdateDataMustBeSet(false);
    } else if (!PublicationDetail.VALID_STATUS.equals(statusChanges.getSecond())) {
      // update last modifier
      pub.setUpdaterId(currentUserId);
    }
    publicationService.setDetail(pub, false, MOVE);

    // check visibility on taxonomy
    updateSilverContentVisibility(pub);

    if (statusChanged) {
      // creates todos for publishers
      createTodosForPublication(pub, false);
      // index or unindex external elements
      if (KmeliaHelper.isIndexable(pub)) {
        indexExternalElementsOfPublication(pub);
      } else {
        unIndexExternalElementsOfPublication(pub.getPK());
      }
    }
    // send notifications like a publish action
    sendSubscriptionsNotification(pub, NotifAction.PUBLISHED, false);
  }

  @Override
  public void externalElementsOfPublicationHaveChanged(PublicationPK pubPK, String userId) {
    externalElementsOfPublicationHaveChanged(pubPK, userId, true);
  }

  private void externalElementsOfPublicationHaveChanged(PublicationPK pubPK, String userId,
      boolean indexExternalElements) {
    // check if related contribution is managed by kmelia
    if (pubPK == null || StringUtil.isNotDefined(pubPK.getInstanceId()) ||
        (!pubPK.getInstanceId().startsWith("kmelia") &&
            !pubPK.getInstanceId().startsWith("toolbox") &&
            !pubPK.getInstanceId().startsWith("kmax"))) {
      return;
    }

    PublicationConcernedByUpdate publicationConcernedByUpdate = new PublicationConcernedByUpdate(
        pubPK).invoke();
    if (publicationConcernedByUpdate.isPublicationNotDefined()) {
      return;
    }

    PublicationDetail pubDetail = publicationConcernedByUpdate.getPubDetail();
    boolean isPublicationInBasketBeforeUpdate =
        publicationConcernedByUpdate.isPublicationInBasketBeforeUpdate();


    if (pubDetail.isClone()) {
      pubDetail.setIndexOperation(IndexManager.NONE);
    }

    if (isDefined(userId)) {
      pubDetail.setUpdaterId(userId);
    }

    // update publication header to store last modifier and update date
    if (!isDefined(userId)) {
      updatePublication(pubDetail, KmeliaHelper.PUBLICATION_CONTENT, false);
    } else {
      updatePublicationAccordingToProfile(userId, pubDetail);
    }

    if (KmeliaHelper.isIndexable(pubDetail) && !isPublicationInBasketBeforeUpdate) {
      publicationService.createIndex(pubDetail);
    }

    if (indexExternalElements) {
      // index all attached files to taking into account visibility period
      indexExternalElementsOfPublication(pubDetail);
    }
  }

  private void updatePublicationAccordingToProfile(final String userId,
      final PublicationDetail pubDetail) {
    // check if user have sufficient rights to update a publication
    String profile = getProfileOnPublication(userId, pubDetail.getPK());
    if ("supervisor".equals(profile) || SilverpeasRole.PUBLISHER.isInRole(profile) ||
        SilverpeasRole.ADMIN.isInRole(profile) || SilverpeasRole.WRITER.isInRole(profile)) {
      updatePublication(pubDetail, KmeliaHelper.PUBLICATION_CONTENT, false);
    } else {
      SilverLogger.getLogger(this)
          .warn("User {0} not allowed to update publication {1}", userId,
              pubDetail.getPK().getId());
    }
  }

  private boolean isClone(PublicationDetail publication) {
    return isDefined(publication.getCloneId()) &&
        !UNDEFINED_NODE_ID.equals(publication.getCloneId()) &&
        !isDefined(publication.getCloneStatus());
  }

  @Override
  @Transactional
  public void deletePublication(PublicationPK pubPK) {
    KmeliaOperationContext.about(DELETION);
    try {
      // remove form content
      removeXMLContentOfPublication(pubPK);
      // delete all reading controls associated to this publication
      deleteAllReadingControlsByPublication(pubPK);
      // delete all links
      publicationService.removeAllFathers(pubPK);
      // delete the publication
      publicationService.deletePublication(pubPK);
      // delete reference to contentManager
      deleteSilverContent(pubPK);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }

  }

  private void sendPublicationInBasket(PublicationPK pubPK, boolean kmaxMode) {
    KmeliaOperationContext.about(REMOVING);
    try {
      // remove coordinates for Kmax
      if (kmaxMode) {
        CoordinatePK coordinatePK =
            new CoordinatePK(UNKNOWN, pubPK.getSpaceId(), pubPK.getComponentName());

        Collection<NodePK> fatherPKs =
            publicationService.getAllFatherPKInSamePublicationComponentInstance(pubPK);
        // delete publication coordinates
        Iterator<NodePK> it = fatherPKs.iterator();
        List<String> coordinates = new ArrayList<>();
        while (it.hasNext()) {
          String coordinateId = (it.next()).getId();
          coordinates.add(coordinateId);
        }
        if (!coordinates.isEmpty()) {
          coordinatesService.deleteCoordinates(coordinatePK, coordinates);
        }
      }

      // remove all links between this publication and topics
      publicationService.removeAllFathers(pubPK);
      // add link between this publication and the basket topic
      publicationService.addFather(pubPK, new NodePK(NodePK.BIN_NODE_ID, pubPK));
      // set removal status
      publicationService.removePublication(pubPK);

      cleanUpPublicationsInBasket(pubPK);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  private void cleanUpPublicationsInBasket(PublicationPK pubPK) {
    // remove all the todos attached to the publication
    removeAllTodosForPublication(pubPK);

    // publication is no more accessible
    updateSilverContentVisibility(pubPK);

    unIndexExternalElementsOfPublication(pubPK);

    publicationService.removePublication(pubPK);
  }

  private void sendPublicationInBasket(PublicationPK pubPK) {
    sendPublicationInBasket(pubPK, KmeliaHelper.isKmax(pubPK.getInstanceId()));
  }

  private void sendTopicInBasket(NodeDetail topic) {
    NodePK trash = new NodePK(NodePK.BIN_NODE_ID,
        topic.getIdentifier().getComponentInstanceId());

    // get all the tree of folders, rooted to the topic, to be sent in the basket with the topic
    List<NodeDetail> nodes = new ArrayList<>();
    nodes.add(topic);
    nodes.addAll(nodeService.getDescendantDetails(topic.getNodePK()));
    for (final NodeDetail node : nodes) {
      // set the removal status of the child
      nodeService.removeNode(node);
      // get all the direct publications in the topic child (including aliases)
      final Collection<PublicationDetail> publications =
          publicationService.getDetailsByFatherPK(node.getNodePK());
      // check each publication: if it is an alias, remove it, otherwise it is moved into the trash
      // with its topic
      for (PublicationDetail publication : publications) {
        final KmeliaPublication kmeliaPublication = fromDetail(publication, node.getNodePK());
        if (kmeliaPublication.isAlias()) {
          // remove only the alias
          final Collection<Location> aliases = singletonList(kmeliaPublication.getLocation());
          publicationService.removeAliases(kmeliaPublication.getPk(), aliases);
        } else {
          cleanUpPublicationsInBasket(publication.getPK());
        }
      }
    }

    nodeService.moveNode(topic.getNodePK(), trash);
  }

  @Override
  @Transactional(Transactional.TxType.REQUIRED)
  public void addPublicationToTopic(PublicationPK pubPK, NodePK fatherPK, boolean isACreation) {
    addPublicationToTopicWithoutNotifications(pubPK, fatherPK, isACreation);
    PublicationDetail pubDetail = getPublicationDetail(pubPK);
    sendSubscriptionsNotification(pubDetail, NotifAction.CREATE, false);
  }

  @Override
  @Transactional(Transactional.TxType.REQUIRED)
  public void addPublicationToTopicWithoutNotifications(PublicationPK pubPK, NodePK fatherPK,
      boolean isACreation) {
    PublicationDetail pubDetail = getPublicationDetail(pubPK);
    Optional<Location> mainPubLocation = publicationService.getMainLocation(pubPK);
    if (!isACreation) {
      try {
        if (mainPubLocation.filter(NodePK::isTrash).isPresent()) {
          publicationService.removeFather(pubPK, mainPubLocation.get());
          mainPubLocation = Optional.empty();
          if (PublicationDetail.VALID_STATUS.equalsIgnoreCase(pubDetail.getStatus())) {
            // index publication
            publicationService.createIndex(pubPK);
            // index external elements
            indexExternalElementsOfPublication(pubDetail);
            // publication is accessible again
            updateSilverContentVisibility(pubDetail);
          } else if (PublicationDetail.TO_VALIDATE_STATUS.equalsIgnoreCase(pubDetail.getStatus())) {
            // create validation todos for publishers
            createTodosForPublication(pubDetail, true);
          }
        } else if (mainPubLocation.isEmpty()) {
          // The publi have got no father
          // change the end date to make this publi visible
          pubDetail.setEndDate(null);
          publicationService.setDetail(pubDetail);
          // publication is accessible again
          updateSilverContentVisibility(pubDetail);
        }
      } catch (Exception e) {
        throw new KmeliaRuntimeException(e);
      }
    }

    try {
      if (mainPubLocation.isPresent()) {
        final Location alias = new Location(fatherPK.getId(), fatherPK.getInstanceId());
        alias.setAsAlias(User.getCurrentRequester().getId());
        publicationService.addAliases(pubPK, singletonList(alias));
      } else {
        publicationService.addFather(pubPK, fatherPK);
      }
      // index publication to index path
      if (pubDetail.isIndexable()) {
        publicationService.createIndex(pubPK);
      }
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  private boolean isPublicationInBasket(PublicationPK pubPK) {
    return publicationService.getAllLocations(pubPK).stream().anyMatch(NodePK::isTrash);
  }

  @SuppressWarnings("unchecked")
  private NodePK sendSubscriptionsNotification(PublicationDetail pubDetail, NotifAction action,
      final boolean sendOnlyToAliases) {
    NodePK father = null;
    // We alert subscribers only if publication is Valid
    if (!pubDetail.haveGotClone() && pubDetail.isValid() && pubDetail.isVisible()) {
      // Subscription to the main topic
      father = getPublicationFatherPK(pubDetail.getPK());
      if (!sendOnlyToAliases && father != null) {
        sendSubscriptionsNotification(father, pubDetail, action);
      }

      // Subscriptions related to aliases
      Collection<Location> locations = (Collection<Location>) getThreadCacheAccessor().getCache()
          .get(ALIASES_CACHE_KEY);
      if (locations == null) {
        locations = getAliases(pubDetail.getPK());
      }
      sendSubscriptionNotificationForAliases(pubDetail, action, locations);

      // PDC subscriptions
      try {
        int silverObjectId = getSilverObjectId(pubDetail.getPK());
        List<ClassifyPosition> positions = pdcManager.getPositions(silverObjectId,
            pubDetail.getPK().getInstanceId());
        if (positions != null) {
          for (ClassifyPosition position : positions) {
            pdcSubscriptionManager.checkSubscriptions(position.getValues(),
                pubDetail.getPK().getInstanceId(), silverObjectId);
          }
        }
      } catch (PdcException e) {
        SilverLogger.getLogger(this)
            .error("PdC subscriptions notification failure for publication {0}",
                new String[]{pubDetail.getPK().getId()}, e);
      }
    } else {
      KmeliaDelayedVisibilityUserNotificationReminder.get().setAbout(pubDetail);
    }
    return father;
  }

  private void sendSubscriptionNotificationForAliases(final PublicationDetail pubDetail,
      final NotifAction action, final Collection<Location> locations) {
    // Transform the current alias to a NodePK (even if Alias is extending NodePK) in the aim
    // to execute the equals method of NodePK
    locations.stream().filter(Location::isAlias).forEach(a -> {
      // we copy the publication detail to avoid overriding attributes of the original one
      final PublicationDetail copy = pubDetail.copy();
      copy.setAlias(true);
      sendSubscriptionsNotification(a, copy, action);
    });
  }

  private void sendSubscriptionsNotification(NodePK fatherPK, PublicationDetail pubDetail,
      NotifAction action) {
    try {
      // Building and sending the notification
      UserNotificationHelper.buildAndSend(
          new KmeliaSubscriptionPublicationUserNotification(fatherPK, pubDetail, action));

    } catch (Exception e) {
      SilverLogger.getLogger(this)
          .error("Subscriber notification failure about publication {0}",
              new String[]{pubDetail.getPK().getId()}, e);
    }
  }

  /**
   * Updates the publication links
   * @param pubPK publication identifier which you want to update links
   * @param links list of publication to link with current.
   */
  @Override
  @Transactional(Transactional.TxType.REQUIRED)
  public void addInfoLinks(PublicationPK pubPK, List<ResourceReference> links) {
    try {
      publicationService.addLinks(pubPK, links);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  @Override
  public CompletePublication getCompletePublication(PublicationPK pubPK) {
    try {
      return publicationService.getCompletePublication(pubPK);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  private KmeliaPublication getPublication(PublicationPK pubPK) {
    return KmeliaPublication.withPK(pubPK);
  }

  @Override
  public KmeliaPublication getPublication(final PublicationPK pubPK, final NodePK topicPK) {
    return KmeliaPublication.withPK(pubPK, topicPK);
  }

  @Override
  public TopicDetail getBestTopicDetailOfPublicationForUser(PublicationPK pubPK,
      boolean isTreeStructureUsed, String userId) {
    // fetch one of the publication fathers
    final NodePK fatherPK = getBestLocationOfPublicationForUser(pubPK, userId);
    final String profile = KmeliaHelper.getProfile(getUserRoles(pubPK.getInstanceId(), userId));
    return goTo(fatherPK, userId, isTreeStructureUsed, profile, false);
  }

  @Override
  public NodePK getBestLocationOfPublicationForUser(PublicationPK pubPK, String userId) {
    final Location root = new Location(NodePK.ROOT_NODE_ID, pubPK.getInstanceId());
    return getPublicationLocations(pubPK, false).stream()
        .sorted(comparing(
            (Location l) -> !l.getInstanceId().equals(pubPK.getInstanceId())).thenComparing(
                Location::isAlias)
            .thenComparing(Location::getInstanceId)
            .thenComparing(Location::getId))
        .filter(l -> NodeAccessControl.get().isUserAuthorized(userId, l))
        .findFirst()
        .orElse(root);
  }

  /**
   * Gets all the locations of the publication in the original Kmelia instance.
   * @param pubPK the identifying key of the publication
   * @param inComponentInstance true o get location in component instance only, false to get all
   * @return a collection of {@link Location} objects.
   */
  private List<Location> getPublicationLocations(PublicationPK pubPK,
      final boolean inComponentInstance) {
    final Function<PublicationPK, List<Location>> locationSupplier = pk -> {
      if (inComponentInstance) {
        return publicationService.getLocationsInComponentInstance(pk, pk.getInstanceId());
      } else {
        return publicationService.getAllLocations(pk);
      }
    };
    List<Location> locations = locationSupplier.apply(pubPK);
    if (locations.isEmpty()) {
      // This publication have got no father!
      // Check if it's a clone (a clone have got no father ever)
      final boolean alwaysVisibleModeActivated = getBooleanValue(
          getOrganisationController().getComponentParameterValue(pubPK.getInstanceId(),
              "publicationAlwaysVisible"));
      if (alwaysVisibleModeActivated) {
        final PublicationDetail publi = publicationService.getDetail(pubPK);
        if (publi != null && isClone(publi)) {
          // This publication is a clone
          // Get fathers from main publication
          locations = locationSupplier.apply(publi.getClonePK());
        }
      }
    }
    return locations;
  }

  @Override
  public NodePK getPublicationFatherPK(PublicationPK pubPK) {
    Collection<Location> locations = getPublicationLocations(pubPK, true);
    return locations.stream().filter(l -> !l.isAlias()).findFirst().orElse(null);
  }

  @Override
  public <T extends ResourceReference> List<PublicationDetail> getPublicationDetails(
      List<T> references) {
    try {
      return publicationService.getPublications(references.stream()
          .map(l -> new PublicationPK(l.getId(), l.getInstanceId()))
          .collect(Collectors.toSet()));
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  @Override
  public <T extends ResourceReference> List<KmeliaPublication> getPublications(
      final List<T> references, String userId, final NodePK contextFolder,
      boolean accessControlFiltering) {
    // initialization of the publications list
    final List<PublicationDetail> publications;
    if (accessControlFiltering) {
      final Map<PublicationPK, ResourceReference> indexedReferences = references.stream()
          .collect(toMap(r -> new PublicationPK(r.getId(), r.getInstanceId()), r -> r));
      final List<ResourceReference> authorizedReferences = PublicationAccessControl.get()
          .filterAuthorizedByUser(indexedReferences.keySet(), userId)
          .map(indexedReferences::get)
          .collect(Collectors.toList());
      publications = getPublicationDetails(authorizedReferences);
    } else {
      publications = getPublicationDetails(references);
    }
    return contextFolder != null ?
        asLocatedKmeliaPublication(contextFolder, publications) :
        asKmeliaPublication(publications);
  }

  @Override
  public <T extends ResourceReference> List<Pair<KmeliaPublication, KmeliaPublication>> getPublicationsForModification(
      final List<T> references, final String userId) {
    // initialization of the publications list
    final Map<PublicationPK, ResourceReference> indexedReferences = references.stream()
        .collect(toMap(r -> new PublicationPK(r.getId(), r.getInstanceId()), r -> r));
    final AccessControlContext modificationContext = AccessControlContext.init()
        .onOperationsOf(MODIFICATION);
    final List<ResourceReference> authorizedReferences = PublicationAccessControl.get()
        .filterAuthorizedByUser(indexedReferences.keySet(), userId, modificationContext)
        .map(indexedReferences::get)
        .collect(Collectors.toList());
    final List<PublicationDetail> publications = getPublicationDetails(authorizedReferences);
    final List<String> pubIds = authorizedReferences.stream()
        .map(ResourceReference::getId)
        .collect(Collectors.toList());
    final Map<String, List<Location>> locationsByPublication =
        publicationService.getAllLocationsByPublicationIds(pubIds);
    final Map<PublicationPK, PublicationDetail> clones =
        getPublicationDetails(publications.stream()
            .filter(HAS_CLONE)
            .map(PublicationDetail::getClonePK)
            .collect(Collectors.toList()))
            .stream()
            .collect(toMap(PublicationDetail::getPK, k -> k));
    return publications.stream()
        .map(p -> KmeliaPublication.fromDetail(p, null, locationsByPublication))
        .map(k -> ofNullable(HAS_CLONE.test(k.getDetail()) ?
            clones.getOrDefault(k.getDetail().getClonePK(), null) : null)
            .map(c -> Pair.of(k, KmeliaPublication.fromDetail(c, k.getLocation(),
                locationsByPublication)))
            .orElseGet(() -> Pair.of(k, null)))
        .collect(Collectors.toList());
  }

  /**
   * Gets the publications linked with the specified one and for which the specified user is
   * authorized to access.
   * @param publication the publication from which linked publications are get.
   * @param userId the unique identifier of a user. It allows to check if a linked publication is
   * accessible for the specified user.
   * @return a list of Kmelia publications.
   * @ if an error occurs while communicating with the remote business service.
   */
  @Override
  public List<KmeliaPublication> getLinkedPublications(KmeliaPublication publication,
      String userId) {
    List<PublicationLink> allLinks = publication.getCompleteDetail().getLinkedPublications(userId);
    List<KmeliaPublication> authorizedLinks = new ArrayList<>();
    for (PublicationLink link : allLinks) {
      authorizedLinks.add(KmeliaPublication.withPK(link.getPubPK()));
    }
    return authorizedLinks;
  }

  @Override
  public List<KmeliaPublication> getPublicationsToValidate(String componentId, String userId) {
    Collection<PublicationDetail> publications = new ArrayList<>();
    try {
      Collection<PublicationDetail> temp = publicationService.getPublicationsByCriteria(
          PublicationCriteria.onComponentInstanceIds(componentId)
              .ofStatus(PublicationDetail.TO_VALIDATE_STATUS)
              .orderByDescendingLastUpdateDate());
      // only publications which must be validated by current user must be returned
      for (PublicationDetail publi : temp) {
        addPublicationsToValidate(userId, publications, publi);
      }
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
    return asKmeliaPublication(publications);
  }

  private void addPublicationsToValidate(final String userId,
      final Collection<PublicationDetail> publications, final PublicationDetail publi) {
    boolean isClone = publi.isValidationRequired() && !UNDEFINED_NODE_ID.equals(publi.getCloneId());
    if (isClone) {
      if (isUserCanValidatePublication(publi.getPK(), userId)) {
        // publication to validate is a clone, get original one
        try {
          PublicationDetail original =
              getPublicationDetail(new PublicationPK(publi.getCloneId(), publi.getPK()));
          publications.add(original);
        } catch (Exception e) {
          // inconsistency in database! Original publication does not exist
          SilverLogger.getLogger(this)
              .warn("Original publication {0} of clone {1} not found", publi.getId(),
                  publi.getCloneId());
        }
      }
    } else {
      if (isUserCanValidatePublication(publi.getPK(), userId)) {
        publications.add(publi);
      }
    }
  }

  private void sendValidationNotification(final NodePK fatherPK, final PublicationDetail pubDetail,
      final String refusalMotive, final String userIdWhoRefuse) {

    try {

      UserNotificationHelper.buildAndSend(
          new KmeliaValidationPublicationUserNotification(fatherPK, pubDetail, refusalMotive,
              userIdWhoRefuse));

    } catch (Exception e) {
      SilverLogger.getLogger(this)
          .error("User notification failure about publication {0}",
              new String[]{pubDetail.getPK().getId()}, e);
    }
  }

  private void sendAlertToSupervisors(final NodePK fatherPK, final PublicationDetail pubDetail) {
    if (pubDetail.isValid()) {
      try {
        UserNotificationHelper.buildAndSend(
            new KmeliaSupervisorPublicationUserNotification(fatherPK, pubDetail));
      } catch (Exception e) {
        SilverLogger.getLogger(this)
            .error("Supervisors notification failure about publication {0}",
                new String[]{pubDetail.getPK().getId()}, e);
      }
    }
  }

  private int getValidationType(String instanceId) {
    String sParam = getOrganisationController().getComponentParameterValue(instanceId,
        InstanceParameters.validation);
    if (isDefined(sParam)) {
      return Integer.parseInt(sParam);
    }
    return KmeliaHelper.VALIDATION_CLASSIC;
  }

  private boolean isTargetedValidationEnabled(String componentId) {
    int value = getValidationType(componentId);
    return value == KmeliaHelper.VALIDATION_TARGET_N || value == KmeliaHelper.VALIDATION_TARGET_1;
  }

  @Override
  public ValidatorsList getAllValidators(PublicationPK pubPK) {
    // get all users who have to validate
    ValidatorsList allValidators = new ValidatorsList(getValidationType(pubPK.getInstanceId()));
    if (isTargetedValidationEnabled(pubPK.getInstanceId())) {
      allValidators.addAll(getActiveValidatorIds(pubPK));
    } else {
      // It's not a targeted validation
      List<String> roles = new ArrayList<>(2);
      roles.add(SilverpeasRole.ADMIN.getName());
      roles.add(SilverpeasRole.PUBLISHER.getName());

      if (KmeliaHelper.isKmax(pubPK.getInstanceId())) {
        allValidators.addAll(Arrays.asList(
            getOrganisationController().getUsersIdsByRoleNames(pubPK.getInstanceId(), roles)));
      } else {
        // get admin and publishers of all nodes where publication is
        addAdminAndPublishers(pubPK, allValidators, roles);
      }
    }
    return allValidators;
  }

  private void addAdminAndPublishers(final PublicationPK pubPK, final List<String> allValidators,
      final List<String> roles) {
    NodePK father = getPublicationFatherPK(pubPK);
    if (father != null) {
      NodeDetail topic = getNodeHeader(father);
      if (!topic.haveRights()) {
        allValidators.addAll(Arrays.asList(
            getOrganisationController().getUsersIdsByRoleNames(pubPK.getInstanceId(), roles)));
      } else {
        allValidators.addAll(Arrays.asList(
            getOrganisationController().getUsersIdsByRoleNames(pubPK.getInstanceId(),
                ProfiledObjectId.fromNode(topic.getRightsDependsOn()), roles)));
      }
    }
  }

  public void setValidators(PublicationPK pubOrClonePK, String userIds) {
    PublicationDetail publication = getPublicationDetail(pubOrClonePK);

    String[] validatorIds = StringUtil.split(userIds, ',');

    if (!ArrayUtil.isEmpty(validatorIds)) {
      // set new validators in database
      publication.setTargetValidatorId(userIds);
      publication.setStatusMustBeChecked(false);
      publication.setIndexOperation(IndexManager.NONE);
      publicationService.setDetail(publication);

      //notify them if the publication (or the clone) is in validation required state...
      if (publication.isValidationRequired()) {
        sendValidationAlert(publication, validatorIds);
      }
    }
  }

  private boolean isValidationComplete(PublicationPK pubPK, List<String> allValidators) {
    List<ValidationStep> steps = publicationService.getValidationSteps(pubPK);

    // get users who have already validate
    List<String> stepUserIds = new ArrayList<>();
    for (ValidationStep step : steps) {
      stepUserIds.add(step.getUserId());
    }

    // check if all users have validate
    boolean validationOK = true;
    for (int i = 0; validationOK && i < allValidators.size(); i++) {
      String validatorId = allValidators.get(i);
      validationOK = stepUserIds.contains(validatorId);
    }

    return validationOK;
  }

  @Override
  public boolean validatePublication(PublicationPK pubPK, String userId, boolean force,
      final boolean hasUserNoMoreValidationRight) {
    boolean validationComplete;
    try {
      CompletePublication currentPub = publicationService.getCompletePublication(pubPK);
      PublicationDetail currentPubDetail = currentPub.getPublicationDetail();
      PublicationDetail currentPubOrCloneDetail = currentPubDetail;
      boolean validationOnClone = currentPubDetail.haveGotClone();
      PublicationPK validatedPK = pubPK;
      if (validationOnClone) {
        validatedPK = currentPubDetail.getClonePK();
        currentPubOrCloneDetail = getPublicationDetail(validatedPK);
      }
      if (!hasUserNoMoreValidationRight && !isUserCanValidatePublication(validatedPK, userId)) {
        SilverLogger.getLogger(this)
            .debug("user ''{0}'' is not allowed to validate publication {1}", userId,
                pubPK.toString());
        return false;
      }

      String validatorUserId = userId;
      Date validationDate = new Date();
      if (force) {
        validationComplete = true;
      } else if (!hasUserNoMoreValidationRight) {
        validationComplete = completeValidation(pubPK, validatedPK, userId, validationOnClone);
      } else {
        ValidationChecker validationChecker = new ValidationChecker().setPubPK(pubPK)
            .setUserId(userId)
            .setCurrentPubDetail(currentPubDetail)
            .setCurrentPubOrCloneDetail(currentPubOrCloneDetail)
            .setValidatedPK(validatedPK)
            .setValidationDate(validationDate)
            .setValidatorUserId(validatorUserId)
            .check();
        validationComplete = validationChecker.isValidationComplete();
        validatorUserId = validationChecker.getValidatorUserId();
        validationDate = validationChecker.getValidationDate();
      }

      if (validationComplete) {
        applyValidation(pubPK, currentPub, currentPubDetail, validatedPK, validatorUserId,
            validationDate, validationOnClone);
      }
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
    return validationComplete;
  }

  private void applyValidation(final PublicationPK pubPK, final CompletePublication currentPub,
      PublicationDetail currentPubDetail, final PublicationPK validatedPK,
      final String validatorUserId, final Date validationDate, final boolean validationOnClone) {
    removeAllTodosForPublication(validatedPK);
    if (validationOnClone) {
      removeAllTodosForPublication(pubPK);
    }
    if (currentPubDetail.haveGotClone()) {
      currentPubDetail = mergeClone(currentPub, validatorUserId, validationDate);
    } else if (currentPubDetail.isValidationRequired()) {
      currentPubDetail.setValidatorId(validatorUserId);
      currentPubDetail.setValidateDate(validationDate);
      currentPubDetail.setStatus(PublicationDetail.VALID_STATUS);
    }
    KmeliaHelper.checkIndex(currentPubDetail);
    publicationService.setDetail(currentPubDetail);
    updateSilverContentVisibility(currentPubDetail);
    // index all publication's elements
    indexExternalElementsOfPublication(currentPubDetail);

    // the publication has been validated
    // all subscribers of the different topics must be alerted
    NodePK oneFather =
        sendSubscriptionsNotification(currentPubDetail, NotifAction.PUBLISHED, false);

    // publication's creator must be alerted
    sendValidationNotification(oneFather, currentPubDetail, null, validatorUserId);

    // alert supervisors
    sendAlertToSupervisors(oneFather, currentPubDetail);
  }

  private boolean completeValidation(final PublicationPK pubPK, final PublicationPK validatedPK,
      final String userId, final boolean validationOnClone) {
    boolean validationComplete = false;
    int validationType = getValidationType(pubPK.getInstanceId());
    if (validationType == KmeliaHelper.VALIDATION_CLASSIC ||
        validationType == KmeliaHelper.VALIDATION_TARGET_1) {
      validationComplete = true;
    } else {
      if (validationType == KmeliaHelper.VALIDATION_TARGET_N) {
        // check that validators are well defined
        // If not, considering validation as classic one
        PublicationDetail publi = publicationService.getDetail(validatedPK);
        validationComplete = !isDefined(publi.getTargetValidatorId());
      }
      if (!validationComplete) {
        // get all users who have to validate
        List<String> allValidators = getAllValidators(validatedPK);
        if (allValidators.size() == 1) {
          // special case : only once user is concerned by validation
          validationComplete = true;
        } else if (allValidators.size() > 1) {
          // remove it for this user. His job is done !
          removeTodoForPublication(validatedPK, userId);
          if (validationOnClone) {
            removeTodoForPublication(pubPK, userId);
          }
          // save his decision

          ValidationStep validation =
              new ValidationStep(validatedPK, userId, PublicationDetail.VALID_STATUS);
          publicationService.addValidationStep(validation);
          // check if all validators have give their decision
          validationComplete = isValidationComplete(validatedPK, allValidators);
        }
      }
    }
    return validationComplete;
  }

  private PublicationDetail getClone(PublicationDetail refPub) {
    PublicationDetail copy = PublicationDetail.builder(refPub.getLanguage())
        .setPk(new PublicationPK(refPub.getPK().getId(), refPub.getPK().getInstanceId()))
        .setNameAndDescription(refPub.getName(), refPub.getDescription())
        .build();
    copy.setAuthor(refPub.getAuthor());
    if (refPub.getBeginDate() != null) {
      copy.setBeginDate(new Date(refPub.getBeginDate().getTime()));
    }
    copy.setBeginHour(refPub.getBeginHour());
    copy.setContentPagePath(refPub.getContentPagePath());
    copy.setCreationDate(new Date(refPub.getCreationDate().getTime()));
    copy.setCreatorId(refPub.getCreatorId());
    if (refPub.getEndDate() != null) {
      copy.setEndDate(new Date(refPub.getEndDate().getTime()));
    }
    copy.setEndHour(refPub.getEndHour());
    copy.setImportance(refPub.getImportance());
    copy.setInfoId(refPub.getInfoId());
    copy.setKeywords(refPub.getKeywords());
    copy.setStatus(refPub.getStatus());
    copy.setTargetValidatorId(refPub.getTargetValidatorId());
    copy.setCloneId(refPub.getCloneId());
    if (refPub.getLastUpdateDate() != null) {
      copy.setUpdateDate(new Date(refPub.getLastUpdateDate().getTime()));
    }
    copy.setUpdaterId(refPub.getUpdaterId());
    if (refPub.getValidateDate() != null) {
      copy.setValidateDate(new Date(refPub.getValidateDate().getTime()));
    }
    copy.setValidatorId(refPub.getValidatorId());
    copy.setVersion(refPub.getVersion());

    return copy;
  }

  /**
   * In charge of merging data from the clone with the stable one.
   * @param currentPub all the necessary data about a publication as {@link CompletePublication}.
   * @param validatorUserId the identifier of the last user validating the given publication.
   * @param validationDate the date of validation to register. Date of day is taken if null is
   * given.
   * @return the merged publication as {@link PublicationDetail}.
   */
  private PublicationDetail mergeClone(CompletePublication currentPub, String validatorUserId,
      final Date validationDate) {
    PublicationDetail currentPubDetail = currentPub.getPublicationDetail();
    String memInfoId = currentPubDetail.getInfoId();
    PublicationPK pubPK = currentPubDetail.getPK();
    // merging clone data on the original ones
    String cloneId = currentPubDetail.getCloneId();
    if (!UNDEFINED_NODE_ID.equals(cloneId)) {
      final PublicationDetail currentClonedPubDetail =
          clonePublication(cloneId, pubPK, validatorUserId, validationDate);
      currentPubDetail = currentClonedPubDetail;
      // merging attachments in a new transaction in order to ensure getting right committed data
      // into JCR
      getTransaction().performNew(() -> {
        final String instanceId = pubPK.getInstanceId();
        final ResourceReference pkFrom = new ResourceReference(pubPK.getId(), instanceId);
        final ResourceReference pkTo = new ResourceReference(cloneId, instanceId);
        final Map<String, String> attachmentIds =
            getAttachmentService().mergeDocuments(pkFrom, pkTo, DocumentType.attachment);
        // merging XMLModel content
        mergeXmlModelClone(pubPK, memInfoId, cloneId, currentClonedPubDetail, attachmentIds);
        // merging Wysiwyg content
        mergeWysiwygClone(pubPK, cloneId, currentClonedPubDetail);
        return null;
      });
      // deleting the clone
      deletePublication(new PublicationPK(cloneId, pubPK));
    }
    return currentPubDetail;
  }

  private void mergeXmlModelClone(final PublicationPK originalPubPK, final String originalInfoId,
      final String cloneId, final PublicationDetail currentClonedPubDetail,
      final Map<String, String> attachmentIds) throws PublicationTemplateException, FormException {
    final String infoId = currentClonedPubDetail.getInfoId();
    if (infoId != null && !"0".equals(infoId) && !isInteger(infoId)) {
      final RecordSet set = getXMLFormFrom(infoId, originalPubPK);
      if (originalInfoId != null && !"0".equals(originalInfoId)) {
        // content already exists
        set.merge(cloneId, originalPubPK.getInstanceId(), originalPubPK.getId(),
            originalPubPK.getInstanceId(), attachmentIds);
      } else {
        // no content exists
        final PublicationTemplateManager publicationTemplateManager =
            PublicationTemplateManager.getInstance();
        publicationTemplateManager.addDynamicPublicationTemplate(
            originalPubPK.getInstanceId() + ":" + infoId, infoId + ".xml");
        set.clone(cloneId, originalPubPK.getInstanceId(), originalPubPK.getId(),
            originalPubPK.getInstanceId(), attachmentIds);
      }
    }
  }

  private void mergeWysiwygClone(final PublicationPK originalPubPK, final String cloneId,
      final PublicationDetail currentClonedPubDetail) {
    final String language = currentClonedPubDetail.getLanguage();
    final boolean cloneWysiwyg = haveGotWysiwyg(originalPubPK.getInstanceId(), cloneId, language);
    if (cloneWysiwyg) {
      try {
        // delete wysiwyg contents of public version
        deleteWysiwygAttachmentsOnly(originalPubPK.getInstanceId(), originalPubPK.getId());
      } catch (WysiwygException e) {
        SilverLogger.getLogger(this).error(e.getMessage(), e);
      }
      // wysiwyg contents of work version become public version ones
      WysiwygController.copy(originalPubPK.getInstanceId(), cloneId, originalPubPK.getInstanceId(),
          originalPubPK.getId(), currentClonedPubDetail.getUpdaterId());
    }
  }

  @Override
  public void unvalidatePublication(PublicationPK pubPK, String userId, String refusalMotive,
      int validationType) {
    try {
      switch (validationType) {
        case KmeliaHelper.VALIDATION_COLLEGIATE:
        case KmeliaHelper.VALIDATION_TARGET_N:
          // reset other decisions
          publicationService.removeValidationSteps(pubPK);
          break;
        case KmeliaHelper.VALIDATION_CLASSIC:
        case KmeliaHelper.VALIDATION_TARGET_1:
        default:
          break;// do nothing
      }

      PublicationDetail currentPubDetail = publicationService.getDetail(pubPK);

      if (currentPubDetail.haveGotClone()) {
        String cloneId = currentPubDetail.getCloneId();
        PublicationPK tempPK = new PublicationPK(cloneId, pubPK);
        PublicationDetail clone = publicationService.getDetail(tempPK);

        // change clone's status
        clone.setStatus("UnValidate");
        clone.setIndexOperation(IndexManager.NONE);
        publicationService.setDetail(clone);

        // Modification de la publication de reference
        currentPubDetail.setCloneStatus(PublicationDetail.REFUSED_STATUS);
        currentPubDetail.setUpdateDataMustBeSet(false);
        publicationService.setDetail(currentPubDetail);

        // we have to alert publication's last updater
        NodePK fatherPK = getPublicationFatherPK(pubPK);
        sendValidationNotification(fatherPK, clone, refusalMotive, userId);

        // remove tasks
        removeAllTodosForPublication(clone.getPK());
      } else {
        // change publication's status
        currentPubDetail.setStatus("UnValidate");

        KmeliaHelper.checkIndex(currentPubDetail);

        publicationService.setDetail(currentPubDetail);

        // change visibility over PDC
        updateSilverContentVisibility(currentPubDetail);

        // we have to alert publication's creator
        NodePK fatherPK = getPublicationFatherPK(pubPK);
        sendValidationNotification(fatherPK, currentPubDetail, refusalMotive, userId);

        //remove tasks
        removeAllTodosForPublication(currentPubDetail.getPK());
      }
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  @Override
  public void suspendPublication(PublicationPK pubPK, String defermentMotive, String userId) {
    try {
      PublicationDetail currentPubDetail = publicationService.getDetail(pubPK);

      // change publication's status
      currentPubDetail.setStatus(PublicationDetail.TO_VALIDATE_STATUS);

      KmeliaHelper.checkIndex(currentPubDetail);

      publicationService.setDetail(currentPubDetail);

      // change visibility over PDC
      updateSilverContentVisibility(currentPubDetail);

      unIndexExternalElementsOfPublication(currentPubDetail.getPK());

      // we have to alert publication's creator
      sendDefermentNotification(currentPubDetail, defermentMotive);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  private void sendDefermentNotification(final PublicationDetail pubDetail,
      final String defermentMotive) {
    try {
      UserNotificationHelper.buildAndSend(
          new KmeliaDefermentPublicationUserNotification(pubDetail, defermentMotive));
    } catch (Exception e) {
      SilverLogger.getLogger(this)
          .warn("User notification failure about publication ''{0}'' (id={1})",
              pubDetail.getTitle(), pubDetail.getPK().getId());
    }
  }

  @Override
  @Transactional(Transactional.TxType.REQUIRED)
  public void draftOutPublication(PublicationPK pubPK, NodePK topicPK, String userProfile) {
    PublicationDetail pubDetail =
        draftOutPublicationWithoutNotifications(pubPK, topicPK, userProfile);
    indexExternalElementsOfPublication(pubDetail);
    sendTodosAndNotificationsOnDraftOut(pubDetail, topicPK, userProfile);
  }

  @Override
  @Transactional(Transactional.TxType.REQUIRED)
  public PublicationDetail draftOutPublicationWithoutNotifications(PublicationPK pubPK,
      NodePK topicPK, String userProfile) {
    return draftOutPublication(pubPK, topicPK, userProfile, false, true);
  }

  @Override
  public void draftOutPublication(PublicationPK pubPK, NodePK topicPK, String userProfile,
      boolean forceUpdateDate) {
    draftOutPublication(pubPK, topicPK, userProfile, forceUpdateDate, false);
  }

  private PublicationDetail draftOutPublication(PublicationPK pubPK, NodePK topicPK,
      String userProfile, boolean forceUpdateDate, boolean inTransaction) {
    try {
      PublicationDetail changedPublication;
      CompletePublication currentPub = publicationService.getCompletePublication(pubPK);
      PublicationDetail pubDetail = currentPub.getPublicationDetail();
      if (userProfile.equals("publisher") || userProfile.equals(ADMIN_ROLE)) {
        if (pubDetail.haveGotClone()) {
          pubDetail = mergeClone(currentPub, null, null);
        }
        pubDetail.setStatus(PublicationDetail.VALID_STATUS);
        changedPublication = pubDetail;
      } else {
        if (pubDetail.haveGotClone()) {
          // changement du statut du clone
          PublicationDetail clone = publicationService.getDetail(pubDetail.getClonePK());
          clone.setStatus(PublicationDetail.TO_VALIDATE_STATUS);
          clone.setIndexOperation(IndexManager.NONE);
          clone.setUpdateDataMustBeSet(false);
          publicationService.setDetail(clone);
          changedPublication = clone;
          pubDetail.setUpdateDataMustBeSet(false);
          pubDetail.setCloneStatus(PublicationDetail.TO_VALIDATE_STATUS);
        } else {
          pubDetail.setStatus(PublicationDetail.TO_VALIDATE_STATUS);
          changedPublication = pubDetail;
        }
      }
      KmeliaHelper.checkIndex(pubDetail);
      publicationService.setDetail(pubDetail, forceUpdateDate);
      if (!KmeliaHelper.isKmax(pubDetail.getInstanceId())) {
        // update visibility attribute on PDC
        updateSilverContentVisibility(pubDetail);
      }
      if (!inTransaction) {
        // index all publication's elements
        indexExternalElementsOfPublication(changedPublication);
        sendTodosAndNotificationsOnDraftOut(changedPublication, topicPK, userProfile);
      }

      return changedPublication;
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  private void sendTodosAndNotificationsOnDraftOut(PublicationDetail pubDetail, NodePK topicPK,
      String userProfile) {
    if (SilverpeasRole.WRITER.isInRole(userProfile)) {
      createTodosForPublication(pubDetail, true);
    }
    // Subscriptions and supervisors are supported by kmelia and filebox only
    if (!KmeliaHelper.isKmax(pubDetail.getInstanceId())) {
      // alert subscribers
      sendSubscriptionsNotification(pubDetail, NotifAction.PUBLISHED, false);

      // alert supervisors
      if (topicPK != null) {
        sendAlertToSupervisors(topicPK, pubDetail);
      }
    }
  }

  @Override
  public void draftInPublication(PublicationPK pubPK) {
    draftInPublication(pubPK, null);
  }

  @Override
  public void draftInPublication(PublicationPK pubPK, String userId) {
    try {
      PublicationDetail pubDetail = publicationService.getDetail(pubPK);
      if (pubDetail.isRefused() || pubDetail.isValid()) {
        pubDetail.setStatus(PublicationDetail.DRAFT_STATUS);
        pubDetail.setUpdaterId(userId);
        KmeliaHelper.checkIndex(pubDetail);
        publicationService.setDetail(pubDetail);
        updateSilverContentVisibility(pubDetail);
        unIndexExternalElementsOfPublication(pubDetail.getPK());
        removeAllTodosForPublication(pubPK);
      }
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  @Override
  public UserNotification getUserNotification(PublicationPK pubPK, NodePK topicPK) {
    final KmeliaPublication publication = KmeliaPublication.withPK(pubPK, topicPK);
    return new KmeliaNotifyPublicationUserNotification(topicPK, publication.getDetail()).build();
  }

  @Override
  public UserNotification getUserNotification(PublicationPK pubPK, SimpleDocumentPK documentPk,
      NodePK topicPK) {
    final PublicationDetail pubDetail = getPublicationDetail(pubPK);
    // componentId of document is always the same than its publication (case of alias)
    documentPk.setComponentName(pubDetail.getInstanceId());
    final SimpleDocument document = getAttachmentService().searchDocumentById(documentPk, null);
    SimpleDocument version = document.getLastPublicVersion();
    if (version == null) {
      version = document.getVersionMaster();
    }
    return new KmeliaNotifyPublicationDocumentUserNotification(topicPK, pubDetail, version).build();
  }

  /**
   * delete reading controls to a publication
   * @param pubPK the id of a publication
   * @since 1.0
   */
  @Override
  public void deleteAllReadingControlsByPublication(PublicationPK pubPK) {
    try {
      statisticService.deleteStats(new ResourceReference(pubPK.getId(), pubPK.getInstanceId()),
          PUBLICATION);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  @Override
  public List<HistoryObjectDetail> getLastAccess(PublicationPK pk, NodePK nodePK,
      String excludedUserId, final int maxResult) {
    final Set<String> userIds = new HashSet<>(getUserIdsOfFolder(nodePK));
    final Set<String> readerIds = new HashSet<>();
    readerIds.add(excludedUserId);
    final Pagination<HistoryObjectDetail> pagination = new Pagination<HistoryObjectDetail>(
        new PaginationPage(1, maxResult)).limitDataSourceCallsTo(10)
        .withMinPerPage(200)
        .paginatedDataSource(
            p -> statisticService.getHistoryByAction(new ResourceReference(pk), 1, PUBLICATION,
                readerIds, p.originalSizeIsNotRequired()))
        .filter(r -> {
          final SilverpeasList<HistoryObjectDetail> currentLastAccess = new SilverpeasArrayList<>();
          for (HistoryObjectDetail access : r) {
            final String readerId = access.getUserId();
            if ((CollectionUtil.isEmpty(userIds) || userIds.contains(readerId)) &&
                !readerIds.contains(readerId)) {
              readerIds.add(readerId);
              if (!User.getById(readerId).isAnonymous()) {
                currentLastAccess.add(access);
              }
            }
          }
          return currentLastAccess;
        });
    try {
      return pagination.execute();
    } finally {
      if (pagination.isNbMaxDataSourceCallLimitReached()) {
        SilverLogger.getLogger(this)
            .warn(
                "Performing too much paginated sql queries to retrieve last accesses from pub {0}" +
                    " on node {1}", pk, nodePK);
      }
    }
  }

  @Override
  public List<String> getUserIdsOfFolder(NodePK pk) {
    if (!isRightsOnTopicsEnabled(pk.getInstanceId())) {
      return Collections.emptyList();
    }

    NodeDetail node = getNodeHeader(pk);

    // check if we have to take care of topic's rights
    if (node != null && node.haveRights()) {
      String rightsDependsOn = node.getRightsDependsOn();
      List<String> profileNames = new ArrayList<>(4);
      profileNames.add(KmeliaHelper.ROLE_ADMIN);
      profileNames.add(KmeliaHelper.ROLE_PUBLISHER);
      profileNames.add(KmeliaHelper.ROLE_WRITER);
      profileNames.add(KmeliaHelper.ROLE_READER);
      String[] userIds = getOrganisationController().getUsersIdsByRoleNames(pk.getInstanceId(),
          ProfiledObjectId.fromNode(rightsDependsOn), profileNames);
      return Arrays.asList(userIds);
    } else {
      return Collections.emptyList();
    }
  }

  @Override
  public void indexKmelia(String componentId) {
    indexTopics(new NodePK(USELESS, componentId));
    indexPublications(componentId);
  }

  private void indexPublications(String componentId) {
    final Collection<PublicationDetail> pubs;
    try {
      pubs = publicationService.getAllPublications(componentId);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }

    if (pubs != null) {
      for (PublicationDetail pub : pubs) {
        processPublicationIndexation(pub);
      }
    }
  }

  private void processPublicationIndexation(final PublicationDetail pub) {
    final PublicationPK pk = pub.getPK();
    try {
      // index only valid publications which are not an alias and not in trash
      if (pub.isValid() && !isPublicationInBasket(pub.getPK())) {
        indexPublication(pub);
      }
    } catch (Exception e) {
      SilverLogger.getLogger(this)
          .error("Error during indexation of publication {0}", pk.getId(), e);
    }
  }

  private void indexPublication(PublicationDetail pub) {
    // index publication itself
    publicationService.createIndex(pub.getPK());

    // index external elements
    indexExternalElementsOfPublication(pub);
  }

  private void indexTopics(NodePK nodePK) {
    try {
      Collection<NodeDetail> nodes = nodeService.getAllNodes(nodePK);
      if (nodes != null) {
        for (NodeDetail node : nodes) {
          if (!node.getNodePK().isRoot() && !node.getNodePK().isTrash() &&
              !node.getNodePK().getId().equals("2")) {
            nodeService.createIndex(node);
          }
        }
      }
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  /*
   * Creates todos for all publishers of this kmelia instance
   * @param pubDetail publication to be validated
   * @param creation true if it's the creation of the publi
   */
  private void createTodosForPublication(PublicationDetail pubDetail, boolean creation) {
    if (!creation) {
      /* remove all todos attached to that publication */
      removeAllTodosForPublication(pubDetail.getPK());
    }
    if (pubDetail.isValidationRequired() ||
        PublicationDetail.TO_VALIDATE_STATUS.equalsIgnoreCase(pubDetail.getCloneStatus())) {
      int validationType = getValidationType(pubDetail.getPK().getInstanceId());
      if (validationType == KmeliaHelper.VALIDATION_TARGET_N ||
          validationType == KmeliaHelper.VALIDATION_COLLEGIATE) {
        // removing potential older validation decision
        publicationService.removeValidationSteps(pubDetail.getPK());
      }
      ValidatorsList validators = getAllValidators(pubDetail.getPK());
      addTodoAndSendNotificationToValidators(pubDetail, validators);
    }
  }

  private void addTodoAndSendNotificationToValidators(PublicationDetail pub,
      ValidatorsList validators) {
    if (CollectionUtil.isNotEmpty(validators)) {
      String[] users = validators.getUserIds();
      addTodo(pub, users);
      // Send a notification to alert validators
      sendValidationAlert(pub, users);
    } else if (validators.isTargetedValidation()) {
      // targeted validation is enabled but there is no validators to notify
      UserNotificationBuilder notification;
      // tries to notify updater
      String profile = getProfileOnPublication(pub.getMostRecentUpdater(), pub.getPK());
      //noinspection ConstantConditions
      if (profile != null &&
          SilverpeasRole.fromString(profile).isGreaterThanOrEquals(SilverpeasRole.WRITER)) {
        notification = new KmeliaNoMoreValidatorPublicationUserNotification(null, pub);
      } else {
        // notify current user
        notification = new KmeliaNoMoreValidatorPublicationUserNotification(null, pub,
            User.getCurrentRequester().getId());
      }
      UserNotificationHelper.buildAndSend(notification);
    }
  }

  private void addTodo(PublicationDetail pubDetail, String[] users) {
    LocalizationBundle message = ResourceLocator.getLocalizationBundle(MESSAGES_PATH);

    TodoDetail todo = new TodoDetail();

    todo.setId(pubDetail.getPK().getId());
    todo.setSpaceId(pubDetail.getPK().getSpace());
    todo.setComponentId(pubDetail.getPK().getComponentName());
    todo.setName(message.getString("ToValidateShort") + " : " + pubDetail.getName());

    List<Attendee> attendees = new ArrayList<>();
    for (String user : users) {
      if (user != null) {
        attendees.add(new Attendee(user));
      }
    }
    todo.setAttendees(new ArrayList<>(attendees));
    todo.setDelegatorId(pubDetail.getMostRecentUpdater());
    todo.setExternalId(pubDetail.getPK().getId());

    calendar.addToDo(todo);
  }

  /*
   * Remove todos for all pubishers of this kmelia instance
   * @param pubDetail corresponding publication
   */
  private void removeAllTodosForPublication(PublicationPK pubPK) {
    calendar.removeToDoFromExternal(USELESS, pubPK.getInstanceId(), pubPK.getId());
  }

  private void removeTodoForPublication(PublicationPK pubPK, List<String> userIds) {
    if (userIds != null) {
      for (String userId : userIds) {
        removeTodoForPublication(pubPK, userId);
      }
    }
  }

  private void removeTodoForPublication(PublicationPK pubPK, String userId) {
    calendar.removeAttendeeInToDoFromExternal(pubPK.getInstanceId(), pubPK.getId(), userId);
  }

  private void sendValidationAlert(final PublicationDetail pubDetail, final String[] users) {
    UserNotificationHelper.buildAndSend(
        new KmeliaPendingValidationPublicationUserNotification(pubDetail, users));
  }

  private void sendModificationAlert(final int modificationScope,
      final PublicationDetail pubDetail) {
    UserNotificationHelper.buildAndSend(
        new KmeliaModificationPublicationUserNotification(pubDetail, modificationScope));
  }

  @Override
  public void sendModificationAlert(int modificationScope, PublicationPK pubPK) {
    sendModificationAlert(modificationScope, getPublicationDetail(pubPK));
  }

  @Override
  public int getSilverObjectId(PublicationPK pubPK) {
    int silverObjectId;
    PublicationDetail pubDetail;
    try {
      silverObjectId =
          kmeliaContentManager.getSilverContentId(pubPK.getId(), pubPK.getInstanceId());
      if (silverObjectId == -1) {
        pubDetail = getPublicationDetail(pubPK);
        silverObjectId = createSilverContent(pubDetail, pubDetail.getCreatorId());
      }
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
    return silverObjectId;
  }

  private int createSilverContent(PublicationDetail pubDetail, String creatorId) {
    Connection con = null;
    try {
      con = getConnection();
      return kmeliaContentManager.createSilverContent(con, pubDetail, creatorId);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    } finally {
      freeConnection(con);
    }
  }

  @Override
  public void deleteSilverContent(PublicationPK pubPK) {
    Connection con = getConnection();
    try {
      kmeliaContentManager.deleteSilverContent(con, pubPK);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    } finally {
      freeConnection(con);
    }
  }

  private void updateSilverContentVisibility(PublicationDetail pubDetail) {
    try {
      kmeliaContentManager.updateSilverContentVisibility(pubDetail);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  private void updateSilverContentVisibility(PublicationPK pubPK) {
    PublicationDetail pubDetail = getPublicationDetail(pubPK);
    try {
      kmeliaContentManager.updateSilverContentVisibility(pubDetail, false);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  private void indexExternalElementsOfPublication(PublicationDetail pubDetail) {
    if (KmeliaHelper.isIndexable(pubDetail)) {
      try {
        // index all files except Wysiwyg which are already indexed as publication content
        List<SimpleDocument> documents = getAttachmentService().listAllDocumentsByForeignKey(
            pubDetail.getPK().toResourceReference(), null);
        for (SimpleDocument doc : documents) {
          if (doc.getDocumentType() != DocumentType.wysiwyg) {
            getAttachmentService().createIndex(doc, pubDetail.getBeginDate(),
                pubDetail.getEndDate());
          }
        }
      } catch (Exception e) {
        SilverLogger.getLogger(this)
            .error("Indexing versioning documents failed for publication {0}",
                new String[]{pubDetail.getPK().getId()}, e);
      }
      try {
        // index comments
        getCommentService().indexAllCommentsOnPublication(pubDetail.getContributionType(),
            new ResourceReference(pubDetail.getPK()));
      } catch (Exception e) {
        SilverLogger.getLogger(this)
            .error("Indexing comments failed for publication {0}",
                new String[]{pubDetail.getPK().getId()}, e);
      }
    }
  }

  private void unIndexExternalElementsOfPublication(PublicationPK pubPK) {
    ResourceReference ref = new ResourceReference(pubPK);
    try {
      getAttachmentService().unindexAttachmentsOfExternalObject(ref);
    } catch (Exception e) {
      SilverLogger.getLogger(this)
          .error("Unindexing versioning documents failed for publication {0}",
              new String[]{pubPK.getId()}, e);
    }
    try {
      // index comments
      getCommentService().unindexAllCommentsOnPublication(PublicationDetail.getResourceType(), ref);
    } catch (Exception e) {
      SilverLogger.getLogger(this)
          .error("Unindexing versioning documents failed for publication {0}",
              new String[]{pubPK.getId()}, e);
    }
  }

  @Override
  public void removeContentOfPublication(PublicationPK pubPK) {
    // remove XML content
    PublicationDetail publication = getPublicationDetail(pubPK);
    if (!StringUtil.isInteger(publication.getInfoId())) {
      removeXMLContentOfPublication(pubPK);
    }
    // reset reference to content
    publication.setInfoId("0");
    updatePublication(publication, KmeliaHelper.PUBLICATION_CONTENT, true);
  }

  private void removeXMLContentOfPublication(PublicationPK pubPK) {
    try {
      PublicationDetail pubDetail = getPublicationDetail(pubPK);
      String infoId = pubDetail.getInfoId();
      if (!isInteger(infoId)) {
        PublicationTemplate pubTemplate = PublicationTemplateManager.getInstance()
            .getPublicationTemplate(pubDetail.getPK().getInstanceId() + ":" + infoId);

        RecordSet set = pubTemplate.getRecordSet();
        set.delete(pubDetail.getPK().getId());
      }
    } catch (PublicationTemplateException | FormException e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  private Connection getConnection() {
    try {
      return DBUtil.openConnection();
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  private void freeConnection(Connection con) {
    if (con != null) {
      try {
        con.close();
      } catch (Exception e) {
        SilverLogger.getLogger(this).error("Connection freed failure", e);
      }
    }
  }

  @Override
  public void setModelUsed(String[] models, String instanceId, String nodeId) {
    Connection con = getConnection();
    try {
      ModelDAO.deleteModel(con, instanceId, nodeId);
      if (models != null) {
        for (String modelId : models) {
          ModelDAO.addModel(con, instanceId, modelId, nodeId);
        }
      }
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    } finally {
      // fermer la connexion
      freeConnection(con);
    }
  }

  @Override
  public Collection<String> getModelUsed(String instanceId, String nodeId) {
    Connection con = getConnection();
    try {
      // get templates defined for the given node
      Collection<String> result = ModelDAO.getModelUsed(con, instanceId, nodeId);
      if (isDefined(nodeId) && result.isEmpty()) {
        // there is no templates defined for the given node, check the parent nodes
        Collection<NodeDetail> parents = nodeService.getPath(new NodePK(nodeId, instanceId));
        Iterator<NodeDetail> iter = parents.iterator();
        while (iter.hasNext() && result.isEmpty()) {
          NodeDetail parent = iter.next();
          //Bin, unclassifieds, unvisibles and to_validate nodes can't have models
          if (!parent.isBin() && !parent.isUnclassified() && !NodePK.UNDEFINED_NODE_ID.equals(parent.getId()) && !KmeliaHelper.SPECIALFOLDER_NONVISIBLEPUBS.equals(parent.getId()) && !KmeliaHelper.SPECIALFOLDER_TOVALIDATE.equals(parent.getId())) {
            result = ModelDAO.getModelUsed(con, instanceId, parent.getNodePK().getId());
          }
        }
      }
      return result;
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    } finally {
      // fermer la connexion
      freeConnection(con);
    }
  }

  /**
   * Copy model used from a node to an other one.
   * @param from the node the models used are linked to.
   * @param to the node the models must be copied.
   */
  private void copyUsedModel(NodePK from, NodePK to) {
    Connection con = getConnection();
    try {
      // get templates defined for the 'from' node
      Collection<String> modelIds = ModelDAO.getModelUsed(con, from.getInstanceId(), from.getId());
      for (String modelId : modelIds) {
        // set template to 'to' node
        ModelDAO.addModel(con, to.getInstanceId(), modelId, to.getId());
      }
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    } finally {
      freeConnection(con);
    }
  }

  @Override
  public List<NodeDetail> getAxis(String componentId) {
    SettingBundle nodeSettings =
        ResourceLocator.getSettingBundle("org.silverpeas.node.nodeSettings");
    String sortField = nodeSettings.getString("sortField", "nodepath");
    String sortOrder = nodeSettings.getString("sortOrder", "asc");
    List<NodeDetail> axis = new ArrayList<>();
    try {
      List<NodeDetail> headers = getAxisHeaders(componentId);
      for (NodeDetail header : headers) {
        // Do not get hidden nodes (Basket and unclassified)
        if (!NodeDetail.STATUS_INVISIBLE.equals(header.getStatus())) {
          // get content  of  this axis
          axis.addAll(nodeService.getSubTree(header.getNodePK(), sortField + " " + sortOrder));
        }
      }
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
    return axis;
  }

  @Override
  public List<NodeDetail> getAxisHeaders(String componentId) {
    List<NodeDetail> axisHeaders;
    try {
      axisHeaders = nodeService.getHeadersByLevel(new NodePK(USELESS, componentId), 2);
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
    return axisHeaders;
  }

  @Override
  public NodePK addAxis(NodeDetail axis, String componentId) {
    NodePK axisPK = new NodePK("toDefine", componentId);
    NodeDetail rootDetail = new NodeDetail("0", "Root", "desc", 1, UNDEFINED_NODE_ID);
    rootDetail.getNodePK().setComponentName(componentId);
    rootDetail.setCreationDate(null);
    rootDetail.setCreatorId(UNKNOWN);
    rootDetail.setPath("/0");
    rootDetail.setStatus(NodeDetail.STATUS_VISIBLE);
    axis.setNodePK(axisPK);
    CoordinatePK coordinatePK = new CoordinatePK(USELESS, axisPK);
    try {
      // axis creation
      NodeDetail createdAxis = nodeService.createNode(axis, rootDetail);
      axisPK = createdAxis.getNodePK();
      // add this new axis to existing coordinates
      CoordinatePoint point = new CoordinatePoint(-1, Integer.parseInt(axisPK.getId()), true);
      coordinatesService.addPointToAllCoordinates(coordinatePK, point);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
    return axisPK;
  }

  @Override
  public void updateAxis(NodeDetail axis, String componentId) {
    axis.getNodePK().setComponentName(componentId);
    final NodeDetail previousNode = nodeService.getDetail(axis.getNodePK());
    axis.setOrder(previousNode.getOrder());
    try {
      nodeService.setDetail(axis);
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
  }

  @Override
  public void deleteAxis(String axisId, String componentId) {
    NodePK pkToDelete = new NodePK(axisId, componentId);

    CoordinatePK coordinatePK = new CoordinatePK(USELESS, pkToDelete);
    // Delete the axis
    try {
      // delete publicationFathers
      final long nbAxisHeaders = getAxisHeaders(componentId).stream()
          .map(NodeDetail::getNodePK)
          .filter(n -> !n.isUnclassed() && !n.isTrash())
          .count();
      if (nbAxisHeaders == 1) {
        final PublicationPK pubPK = new PublicationPK(USELESS, componentId);
        final List<String> fatherIds =
            coordinatesService.getCoordinateIdsByNodeId(coordinatePK, axisId)
                .stream()
                .filter(f -> !NodePK.UNCLASSED_NODE_ID.equals(f) && !NodePK.BIN_NODE_ID.equals(f))
                .collect(Collectors.toList());
        if (!fatherIds.isEmpty()) {
          publicationService.removeFathers(pubPK, fatherIds);
        }
      }
      // delete coordinate which contains subComponents of this component
      Collection<NodeDetail> subComponents = nodeService.getDescendantDetails(pkToDelete);
      Iterator<NodeDetail> it = subComponents.iterator();
      List<NodePK> points = new ArrayList<>();
      points.add(pkToDelete);
      while (it.hasNext()) {
        points.add((it.next()).getNodePK());
      }
      removeCoordinatesByPoints(points, componentId);
      // delete axis
      nodeService.deleteNode(pkToDelete);
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
  }

  private void removeCoordinatesByPoints(List<NodePK> nodePKs, String componentId) {
    Iterator<NodePK> it = nodePKs.iterator();
    List<String> coordinatePoints = new ArrayList<>();
    String nodeId;
    while (it.hasNext()) {
      nodeId = (it.next()).getId();
      coordinatePoints.add(nodeId);
    }
    CoordinatePK coordinatePK = new CoordinatePK(USELESS, USELESS, componentId);
    try {
      coordinatesService.deleteCoordinatesByPoints(coordinatePK, coordinatePoints);
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
  }

  @Override
  public NodeDetail getNodeHeader(String id, String componentId) {
    NodePK pk = new NodePK(id, componentId);
    return getNodeHeader(pk);
  }

  private NodeDetail getNodeHeader(NodePK pk) {
    NodeDetail nodeDetail;
    try {
      nodeDetail = nodeService.getHeader(pk);
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
    return nodeDetail;
  }

  @Override
  public void addPosition(String fatherId, NodeDetail position, String componentId,
      String userId) {
    position.getNodePK().setComponentName(componentId);
    position.setCreationDate(new Date());
    position.setCreatorId(userId);
    NodeDetail fatherDetail = getNodeHeader(fatherId, componentId);
    try {
      nodeService.createNode(position, fatherDetail);
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
  }

  @Override
  public void updatePosition(NodeDetail position, String componentId) {
    position.getNodePK().setComponentName(componentId);
    try {
      nodeService.setDetail(position);
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
  }

  @Override
  public void deletePosition(String positionId, String componentId) {
    NodePK pkToDelete = new NodePK(positionId, componentId);
    // Delete the axis
    try {
      // delete coordinate which contains subPositions of this position
      Collection<NodeDetail> subComponents = nodeService.getDescendantDetails(pkToDelete);
      Iterator<NodeDetail> it = subComponents.iterator();
      List<NodePK> points = new ArrayList<>();
      points.add(pkToDelete);
      while (it.hasNext()) {
        points.add((it.next()).getNodePK());
      }
      removeCoordinatesByPoints(points, componentId);
      // delete component
      nodeService.deleteNode(pkToDelete);
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
  }

  @Override
  public Collection<NodeDetail> getPath(String id, String componentId) {
    Collection<NodeDetail> newPath = new ArrayList<>();
    NodePK nodePK = new NodePK(id, componentId);
    // compute path from a to z

    try {
      List<NodeDetail> pathInReverse = nodeService.getPath(nodePK);
      // reverse the path from root to leaf
      for (int i = pathInReverse.size() - 1; i >= 0; i--) {
        newPath.add(pathInReverse.get(i));
      }
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
    return newPath;
  }

  @Override
  public List<KmeliaPublication> search(List<String> combination, String componentId) {
    Collection<PublicationDetail> publications = searchPublications(combination, componentId);
    if (publications == null) {
      return new ArrayList<>();
    }
    return asKmeliaPublication(publications);
  }

  @Override
  public List<KmeliaPublication> search(List<String> combination, int nbDays, String componentId) {
    Collection<PublicationDetail> publications = searchPublications(combination, componentId);
    return asKmeliaPublication(filterPublicationsByBeginDate(publications, nbDays));
  }

  private Collection<PublicationDetail> searchPublications(List<String> combination,
      String componentId) {
    PublicationPK pk = new PublicationPK(USELESS, componentId);
    CoordinatePK coordinatePK = new CoordinatePK(UNKNOWN, pk);
    Collection<PublicationDetail> publications = null;
    Collection<String> coordinates;
    try {
      // Remove node "Toutes catégories" (level == 2) from combination
      int nodeLevel;
      String axisValue;
      int i = 0;
      while (i < combination.size()) {
        axisValue = combination.get(i);
        StringTokenizer st = new StringTokenizer(axisValue, "/");
        nodeLevel = st.countTokens();
        // if node is level 2, it represents "Toutes Catégories"
        // this axis is not used by the search
        if (nodeLevel == 2) {
          combination.remove(i);
          i--;
        }
        i++;
      }
      if (combination.isEmpty()) {
        // all criterias is "Toutes Catégories"
        // get all publications classified
        NodePK basketPK = new NodePK("1", componentId);
        publications = publicationService.getDetailsNotInFatherPK(basketPK);
      } else {
        coordinates = coordinatesService.getCoordinatesByFatherPaths(combination, coordinatePK);
        if (coordinates != null && !coordinates.isEmpty()) {
          publications =
              publicationService.getDetailsByFatherIds((ArrayList<String>) coordinates, componentId,
                  false);
        }
      }
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
    return publications;
  }

  @Override
  public Collection<KmeliaPublication> getUnbalancedPublications(String componentId) {
    Collection<PublicationDetail> publications;
    try {
      publications = publicationService.getOrphanPublications(componentId);
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
    return asKmeliaPublication(publications);
  }

  private Collection<PublicationDetail> filterPublicationsByBeginDate(
      Collection<PublicationDetail> publications, int nbDays) {
    List<PublicationDetail> pubOK = new ArrayList<>();
    if (publications != null) {
      Calendar rightNow = Calendar.getInstance();
      if (nbDays == 0) {
        nbDays = 1;
      }
      rightNow.add(Calendar.DATE, -nbDays);
      Date day = rightNow.getTime();
      Iterator<PublicationDetail> it = publications.iterator();
      PublicationDetail pub;
      Date dateToCompare;
      while (it.hasNext()) {
        pub = it.next();
        if (pub.getBeginDate() != null) {
          dateToCompare = pub.getBeginDate();
        } else {
          dateToCompare = pub.getCreationDate();
        }

        if (dateToCompare.compareTo(day) >= 0) {
          pubOK.add(pub);
        }
      }
    }
    return pubOK;
  }

  @Override
  public void indexKmax(String componentId) {
    indexAxis(componentId);
    indexPublications(componentId);
  }

  private void indexAxis(String componentId) {
    NodePK nodePK = new NodePK(USELESS, componentId);
    try {
      Collection<NodeDetail> nodes = nodeService.getAllNodes(nodePK);
      if (nodes != null) {
        for (NodeDetail nodeDetail : nodes) {
          if (!"corbeille".equalsIgnoreCase(nodeDetail.getName()) ||
              !nodeDetail.getNodePK().isTrash()) {
            nodeService.createIndex(nodeDetail);
          }
        }
      }
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
  }

  @Override
  public KmeliaPublication getKmaxPublication(String pubId, String currentUserId) {
    PublicationPK pubPK;
    CompletePublication completePublication;

    try {
      pubPK = new PublicationPK(pubId);
      completePublication = publicationService.getCompletePublication(pubPK);
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
    return KmeliaPublication.aKmeliaPublicationFromCompleteDetail(completePublication);
  }

  @Override
  public Collection<Coordinate> getPublicationCoordinates(String pubId, String componentId) {
    try {
      return publicationService.getCoordinates(pubId, componentId);
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
  }

  @Override
  public void addPublicationToCombination(String pubId, List<String> combination,
      String componentId) {
    PublicationPK pubPK = new PublicationPK(pubId, componentId);
    CoordinatePK coordinatePK = new CoordinatePK(UNKNOWN, pubPK);
    try {

      Collection<Coordinate> coordinates = getPublicationCoordinates(pubId, componentId);

      if (!checkCombination(coordinates, combination)) {
        return;
      }

      NodeDetail nodeDetail;
      // enrich combination by get ancestors
      Iterator<String> it = combination.iterator();
      List<CoordinatePoint> allnodes = new ArrayList<>();
      int i = 1;
      while (it.hasNext()) {
        String nodeId = it.next();
        NodePK nodePK = new NodePK(nodeId, componentId);
        Collection<NodeDetail> path = nodeService.getPath(nodePK);
        for (NodeDetail aPath : path) {
          nodeDetail = aPath;
          String anscestorId = nodeDetail.getNodePK().getId();
          int nodeLevel = nodeDetail.getLevel();
          if (!nodeDetail.getNodePK().isRoot()) {
            CoordinatePoint point;
            if (anscestorId.equals(nodeId)) {
              point = new CoordinatePoint(-1, Integer.parseInt(anscestorId), true, nodeLevel, i);
            } else {
              point = new CoordinatePoint(-1, Integer.parseInt(anscestorId), false, nodeLevel, i);
            }
            allnodes.add(point);
          }
        }
        i++;
      }
      int coordinateId = coordinatesService.addCoordinate(coordinatePK, allnodes);
      publicationService.addFather(pubPK, new NodePK(String.valueOf(coordinateId), pubPK));
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
  }

  @SuppressWarnings("unchecked")
  protected boolean checkCombination(Collection<Coordinate> coordinates, List<String> combination) {
    for (Coordinate coordinate : coordinates) {
      Collection<CoordinatePoint> points = coordinate.getCoordinatePoints();
      if (points.isEmpty()) {
        continue;
      }

      boolean matchFound = false;

      for (CoordinatePoint point : points) {
        if (!checkPoint(point, combination)) {
          matchFound = false;
          break;
        }
        matchFound = true;
      }

      if (matchFound) {
        return false;
      }
    }
    return true;
  }

  protected boolean checkPoint(CoordinatePoint point, List<String> combination) {
    for (String intVal : combination) {
      if (Integer.parseInt(intVal) == point.getNodeId()) {
        return true;
      }
    }
    return false;
  }

  @Override
  public void deletePublicationFromCombination(String pubId, String combinationId,
      String componentId) {
    PublicationPK pubPK = new PublicationPK(pubId, componentId);
    NodePK fatherPK = new NodePK(combinationId, componentId);
    CoordinatePK coordinatePK = new CoordinatePK(combinationId, pubPK);
    try {
      // remove publication fathers
      publicationService.removeFather(pubPK, fatherPK);
      // remove coordinate
      List<String> coordinateIds = new ArrayList<>(1);
      coordinateIds.add(combinationId);
      coordinatesService.deleteCoordinates(coordinatePK, coordinateIds);
    } catch (Exception e) {
      throw new KmaxRuntimeException(e);
    }
  }

  /**
   * Create a new Publication (only the header - parameters)
   * @param pubDetail a PublicationDetail
   * @return the id of the new publication
   * @see org.silverpeas.core.contribution.publication.model.PublicationDetail
   * @since 1.0
   */
  @Override
  public String createKmaxPublication(PublicationDetail pubDetail) {
    PublicationPK pubPK;
    Connection con = getConnection();
    try {
      // create the publication
      changePublicationStatusOnCreation(pubDetail, new NodePK(USELESS, pubDetail.getPK()));
      pubPK = publicationService.createPublication(pubDetail);
      pubDetail.getPK().setId(pubPK.getId());

      // creates todos for publishers
      this.createTodosForPublication(pubDetail, true);

      // register the new publication as a new content to content manager
      createSilverContent(pubDetail, pubDetail.getCreatorId());
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    } finally {
      freeConnection(con);
    }
    return pubPK.getId();
  }

  @Override
  public Collection<Location> getAliases(final PublicationPK pubPK) {
    try {
      return publicationService.getAllAliases(pubPK);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  @Override
  public Collection<Location> getLocations(PublicationPK pubPK) {
    try {
      Collection<Location> locations = publicationService.getAllLocations(pubPK);
      if (locations.isEmpty()) {
        // This publication doesn't yet have any location!
        // Check if it's a clone (a clone doesn't have a location. It is orphaned)
        boolean alwaysVisibleModeActivated = getBooleanValue(
            getOrganisationController().getComponentParameterValue(pubPK.getInstanceId(),
                "publicationAlwaysVisible"));
        if (alwaysVisibleModeActivated) {
          PublicationDetail publication = publicationService.getDetail(pubPK);
          if (publication != null && isClone(publication)) {
            // This publication is a clone
            // Get locations from the main publication
            locations = publicationService.getAllLocations(publication.getClonePK());
          }
        }
      }
      return locations;
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  @Override
  public void setAliases(PublicationPK pubPK, List<Location> locations) {

    Pair<Collection<Location>, Collection<Location>> result = publicationService.setAliases(pubPK,
        locations);

    // Send subscriptions to aliases subscribers
    getThreadCacheAccessor().getCache().put(ALIASES_CACHE_KEY, result.getFirst());
    PublicationDetail pubDetail = getPublicationDetail(pubPK);
    sendSubscriptionsNotification(pubDetail, NotifAction.PUBLISHED, true);
  }

  @Override
  @Transactional(Transactional.TxType.NOT_SUPPORTED)
  public void addAttachmentToPublication(PublicationPK pubPK, String userId, String filename,
      String description, byte[] contents) {
    try {
      Date creationDate = new Date();
      SimpleAttachment file = SimpleAttachment.builder(I18NHelper.DEFAULT_LANGUAGE)
          .setFilename(FileUtil.getFilename(filename))
          .setTitle(filename)
          .setDescription("")
          .setSize(contents.length)
          .setContentType(FileUtil.getMimeType(filename))
          .setCreationData(userId, creationDate)
          .build();
      boolean versioningActive = getBooleanValue(
          getOrganisationController().getComponentParameterValue(pubPK.getComponentName(),
              VERSION_MODE));
      SimpleDocument document;
      if (versioningActive) {
        document = new HistorisedDocument(new SimpleDocumentPK(null, pubPK.getComponentName()),
            pubPK.getId(), 0, file);
        document.setPublicDocument(true);
        document.setDocumentType(DocumentType.attachment);
      } else {
        document =
            new SimpleDocument(new SimpleDocumentPK(null, pubPK.getComponentName()), pubPK.getId(),
                0, false, file);
      }
      getAttachmentService().createAttachment(document, new ByteArrayInputStream(contents));
    } catch (org.silverpeas.core.contribution.attachment.AttachmentException fnfe) {
      throw new KmeliaRuntimeException(fnfe);
    }
  }

  @Override
  public NodeDetail createTopic(String componentId, String topicId, String spaceId, String userId,
      String name, String description) {
    NodeDetail topic = new NodeDetail(NodePK.UNDEFINED_NODE_ID, name, description, 0, "X");
    topic.getNodePK().setSpace(spaceId);
    topic.getNodePK().setComponentName(componentId);
    topic.setCreatorId(userId);

    NodePK fatherPK = new NodePK(topicId, spaceId, componentId);
    String alertType = "None";

    return addSubTopic(fatherPK, topic, alertType);
  }

  @Override
  public String clonePublication(CompletePublication refPubComplete, PublicationDetail pubDetail,
      String nextStatus) {
    String cloneId;
    try {
      // récupération de la publi de référence
      PublicationDetail refPub = refPubComplete.getPublicationDetail();

      String fromId = refPub.getPK().getId();
      String fromComponentId = refPub.getPK().getInstanceId();

      PublicationDetail clone = getClone(refPub);

      SettingBundle publicationSettings = ResourceLocator.getSettingBundle(
          "org.silverpeas.publication.publicationSettings");
      String absolutePath = FileRepositoryManager.getAbsolutePath(fromComponentId);

      if (pubDetail != null) {
        clone.setAuthor(pubDetail.getAuthor());
        clone.setBeginDate(pubDetail.getBeginDate());
        clone.setBeginHour(pubDetail.getBeginHour());
        clone.setDescription(pubDetail.getDescription());
        clone.setEndDate(pubDetail.getEndDate());
        clone.setEndHour(pubDetail.getEndHour());
        clone.setImportance(pubDetail.getImportance());
        clone.setKeywords(pubDetail.getKeywords());
        clone.setName(pubDetail.getName());
        clone.setTargetValidatorId(pubDetail.getTargetValidatorId());
      }
      if (isInteger(refPub.getInfoId())) {
        // Case content = DB
        clone.setInfoId(null);
      }
      clone.setStatus(nextStatus);
      clone.setCloneId(fromId);
      clone.setIndexOperation(IndexManager.NONE);

      PublicationPK clonePK = publicationService.createPublication(clone);
      clonePK.setComponentName(fromComponentId);
      cloneId = clonePK.getId();

      // clone attachments
      List<SimpleDocument> documents = getAttachmentService().listDocumentsByForeignKey(
          new ResourceReference(fromId, fromComponentId), null);
      Map<String, String> attachmentIds = new HashMap<>(documents.size());
      Collections.reverse(documents);
      for (SimpleDocument document : documents) {
        getAttachmentService().cloneDocument(document, cloneId);
      }

      // eventually, paste the form content
      String xmlFormShortName = refPub.getInfoId();
      if (xmlFormShortName != null && !"0".equals(xmlFormShortName) &&
          !isInteger(xmlFormShortName)) {
        PublicationTemplateManager templateManager = PublicationTemplateManager.getInstance();
        // Content = XMLForm
        // register xmlForm to publication
        templateManager.addDynamicPublicationTemplate(fromComponentId + ":" + xmlFormShortName,
            xmlFormShortName + ".xml");

        PublicationTemplate pubTemplate =
            templateManager.getPublicationTemplate(fromComponentId + ":" + xmlFormShortName);

        RecordSet set = pubTemplate.getRecordSet();

        // clone dataRecord
        set.clone(fromId, fromComponentId, cloneId, fromComponentId, attachmentIds);
      }

      // paste wysiwyg
      WysiwygController.copy(fromComponentId, fromId, fromComponentId, cloneId,
          clone.getCreatorId());

      // affectation de l'id du clone à la publication de référence
      refPub.setCloneId(cloneId);
      refPub.setCloneStatus(nextStatus);
      refPub.setStatusMustBeChecked(false);
      refPub.setUpdateDataMustBeSet(false);
      updatePublication(refPub);

      // paste vignette
      String vignette = refPub.getImage();
      if (vignette != null) {
        ThumbnailDetail thumbDetail = new ThumbnailDetail(clone.getPK().getInstanceId(),
            Integer.parseInt(clone.getPK().getId()),
            ThumbnailDetail.THUMBNAIL_OBJECTTYPE_PUBLICATION_VIGNETTE);
        thumbDetail.setMimeType(refPub.getImageMimeType());
        if (vignette.startsWith("/")) {
          thumbDetail.setOriginalFileName(vignette);
        } else {
          String thumbnailsSubDirectory = publicationSettings.getString("imagesSubDirectory");
          String from = absolutePath + thumbnailsSubDirectory + File.separator + vignette;
          String type = FilenameUtils.getExtension(vignette);
          String newVignette = System.currentTimeMillis() + "." + type;
          String to = absolutePath + thumbnailsSubDirectory + File.separator + newVignette;
          FileRepositoryManager.copyFile(from, to);
          thumbDetail.setOriginalFileName(newVignette);
        }
        ThumbnailServiceProvider.getThumbnailService().createThumbnail(thumbDetail);
      }
    } catch (IOException | ThumbnailException | FormException | PublicationTemplateException e) {
      throw new KmeliaRuntimeException(e);
    }
    return cloneId;
  }

  /**
   * Gets a service object on the comments.
   * @return a DefaultCommentService instance.
   */
  private CommentService getCommentService() {
    return commentService;
  }

  private LocalizationBundle getMultilang() {
    return ResourceLocator.getLocalizationBundle(MESSAGES_PATH);
  }

  @Override
  public NodeDetail getRoot(String componentId, String userId) {
    return getRoot(componentId, userId, null);
  }

  private NodeDetail getRoot(String componentId, String userId, List<NodeDetail> treeview) {
    NodePK rootPK = new NodePK(NodePK.ROOT_NODE_ID, componentId);
    NodeDetail root = nodeService.getDetail(rootPK);
    setRole(root, userId);
    root.setChildrenDetails(getRootChildren(root, userId, treeview));
    return root;
  }

  private List<NodeDetail> getRootChildren(NodeDetail root, String userId,
      List<NodeDetail> treeview) {
    String instanceId = root.getNodePK().getInstanceId();
    List<NodeDetail> children = new ArrayList<>();
    try {
      setAllowedSubfolders(root, userId);
      List<NodeDetail> nodes = (List<NodeDetail>) root.getChildrenDetails();

      // set nb objects in nodes
      setNbItemsOfSubfolders(root, treeview, userId);

      NodeDetail trash = null;
      for (NodeDetail node : nodes) {
        if (node.getNodePK().isTrash()) {
          trash = node;
        } else if (!node.getNodePK().isUnclassed()) {
          children.add(node);
        }
      }

      // adding special folder "to validate"
      if (isUserCanValidate(instanceId, userId)) {
        NodeDetail temp = new NodeDetail();
        temp.getNodePK().setId(KmeliaHelper.SPECIALFOLDER_TOVALIDATE);
        temp.setName(getMultilang().getString("ToValidateShort"));
        if (isNbItemsDisplayed(instanceId)) {
          int nbPublisToValidate = getPublicationsToValidate(instanceId, userId).size();
          temp.setNbObjects(nbPublisToValidate);
        }
        children.add(temp);
      }

      // adding special folders "non visible publications"
      if (isUserCanWrite(instanceId, userId)) {
        NodeDetail temp = new NodeDetail();
        temp.getNodePK().setId(KmeliaHelper.SPECIALFOLDER_NONVISIBLEPUBS);
        temp.setName(getMultilang().getString("kmelia.folder.nonvisiblepubs"));
        if (isNbItemsDisplayed(instanceId)) {
          int nbPublis = getNonVisiblePublications(instanceId, userId).size();
          temp.setNbObjects(nbPublis);
        }
        children.add(temp);

        if (trash != null) {
          children.add(trash);
        }
      }

      root.setChildrenDetails(children);
    } catch (Exception e) {
      throw new WebApplicationException(Status.INTERNAL_SERVER_ERROR);
    }
    return children;
  }

  @Override
  public NodeDetail getFolder(final NodePK nodePK, final String userId) {
    NodeDetail node = nodeService.getDetail(nodePK);
    if (node.getNodePK().isRoot()) {
      node.setChildrenDetails(getRootChildren(node, userId, null));
    } else {
      setAllowedSubfolders(node, userId);
    }

    // set nb objects in nodes
    final List<NodeDetail> treeView = getTreeview(nodePK, userId);
    setNbItemsOfFolders(node.getNodePK().getInstanceId(), singletonList(node), treeView);
    setNbItemsOfSubfolders(node, treeView, userId);
    return node;
  }

  @Override
  public Collection<NodeDetail> getFolderChildren(NodePK nodePK, String userId) {
    NodeDetail node = getFolder(nodePK, userId);
    return node.getChildrenDetails();
  }

  private void setNbItemsOfSubfolders(NodeDetail node, List<NodeDetail> treeview, String userId) {
    String instanceId = node.getNodePK().getInstanceId();
    if (isNbItemsDisplayed(instanceId)) {
      if (treeview == null) {
        treeview = getTreeview(node.getNodePK(), userId);
      }
      // set nb objects in each nodes
      setNbItemsOfFolders(instanceId, node.getChildrenDetails(), treeview);
    }
  }

  private List<NodeDetail> getTreeview(NodePK pk, String userId) {
    String instanceId = pk.getInstanceId();
    if (isUserComponentAdmin(instanceId, userId)) {
      return getTreeview(pk, ADMIN_ROLE, isCoWritingEnable(instanceId),
          isDraftVisibleWithCoWriting(), userId, isNbItemsDisplayed(instanceId), false);
    } else {
      return getTreeview(pk, getUserTopicProfile(pk, userId), isCoWritingEnable(instanceId),
          isDraftVisibleWithCoWriting(), userId, isNbItemsDisplayed(instanceId),
          isRightsOnTopicsEnabled(instanceId));
    }
  }

  private void setNbItemsOfFolders(String componentId, Collection<NodeDetail> nodes,
      List<NodeDetail> treeview) {
    if (isNbItemsDisplayed(componentId)) {
      for (NodeDetail child : nodes) {
        int index = treeview.indexOf(child);
        if (index != -1) {
          child.setNbObjects(treeview.get(index).getNbObjects());
        }
      }
    }
  }

  private void setAllowedSubfolders(NodeDetail node, String userId) {
    String instanceId = node.getNodePK().getInstanceId();
    if (isRightsOnTopicsEnabled(instanceId)) {
      if (isUserComponentAdmin(instanceId, userId) ||
          SilverpeasRole.ADMIN.getName().equals(getUserTopicProfile(node.getNodePK(), userId))) {
        // user is admin of application or admin of folder, all subfolders must be shown
        setRole(node.getChildrenDetails(), userId);
      } else {
        Collection<NodeDetail> allowedChildren = getAllowedSubfolders(node, userId);
        setRole(allowedChildren, userId);
        node.setChildrenDetails(allowedChildren);
      }
    }
  }

  private boolean isUserComponentAdmin(String componentId, String userId) {
    return ADMIN_ROLE.equalsIgnoreCase(KmeliaHelper.getProfile(getUserRoles(componentId, userId)));
  }

  private void setRole(NodeDetail node, String userId) {
    if (isRightsOnTopicsEnabled(node.getNodePK().getInstanceId())) {
      node.setUserRole(getUserTopicProfile(node.getNodePK(), userId));
    }
  }

  private void setRole(Collection<NodeDetail> nodes, String userId) {
    for (NodeDetail node : nodes) {
      setRole(node, userId);
    }
  }

  private boolean isRightsOnTopicsEnabled(String componentId) {
    return getBooleanValue(getOrganisationController().getComponentParameterValue(componentId,
        InstanceParameters.rightsOnFolders));
  }

  private boolean isNbItemsDisplayed(String componentId) {
    return getBooleanValue(getOrganisationController().getComponentParameterValue(componentId,
        InstanceParameters.displayNbItemsOnFolders));
  }

  private boolean isCoWritingEnable(String componentId) {
    return getBooleanValue(getOrganisationController().getComponentParameterValue(componentId,
        InstanceParameters.coWriting));
  }

  private boolean isDraftVisibleWithCoWriting() {
    return getComponentSettings().getBoolean("draftVisibleWithCoWriting", false);
  }

  @Override
  public String getUserTopicProfile(NodePK pk, String userId) {
    if (!isRightsOnTopicsEnabled(pk.getInstanceId()) || KmeliaHelper.isSpecialFolder(pk.getId())) {
      return KmeliaHelper.getProfile(getUserRoles(pk.getInstanceId(), userId));
    }

    NodeDetail node = getNodeHeader(pk.getId(), pk.getInstanceId());

    // check if we have to take care of topic's rights
    if (node != null && node.haveRights()) {
      String rightsDependsOn = node.getRightsDependsOn();
      return KmeliaHelper.getProfile(
          getOrganisationController().getUserProfiles(userId, pk.getInstanceId(),
              ProfiledObjectId.fromNode(rightsDependsOn)));
    } else {
      return KmeliaHelper.getProfile(getUserRoles(pk.getInstanceId(), userId));
    }
  }

  private String[] getUserRoles(String componentId, String userId) {
    final OrganizationController oc = getOrganisationController();
    String[] profiles = oc.getUserProfiles(userId, componentId);
    if (ArrayUtil.isEmpty(profiles) && oc.getComponentInstLight(componentId).isPublic()) {
      profiles = new String[]{KmeliaHelper.ROLE_READER};
    }
    return profiles;
  }

  private NodePK getRootPK(String componentId) {
    return new NodePK(NodePK.ROOT_NODE_ID, componentId);
  }

  /**
   * This method verifies if the user behind the given user identifier can validate the publication
   * represented by the given primary key. The verification is strictly applied on the given primary
   * key, that is to say that no publication clone information are retrieved. To perform a
   * verification on a publication clone, the primary key of the clone must be given.
   * @param pubPK the primary key of the publication or of the clone of a publication.
   * @param userId the identifier of the user fo which rights must be verified.
   * @return true if the user can validate, false otherwise.
   */
  @Override
  public boolean isUserCanValidatePublication(PublicationPK pubPK, String userId) {
    PublicationDetail publi = getPublicationDetail(pubPK);
    if (!publi.isValidationRequired()) {
      // publication is not in a state which allow a validation
      return false;
    }
    final ValidatorsList validators = getAllValidators(pubPK);
    if (!validators.contains(userId)) {
      // current user is not part of users who are able to validate this publication
      return false;
    }

    if (validators.getValidationType() == KmeliaHelper.VALIDATION_TARGET_N) {
      ValidationStep validationStep = publicationService.getValidationStepByUser(pubPK, userId);
      // user has not yet validated publication, so validation is allowed
      return validationStep == null;
    }
    return true;
  }

  @Override
  public boolean isUserCanValidate(String componentId, String userId) {
    if (isToolbox(componentId)) {
      return false;
    }
    return isUserCanPublish(componentId, userId);
  }

  @Override
  public boolean isUserCanWrite(String componentId, String userId) {
    String[] grantedRoles =
        new String[]{SilverpeasRole.ADMIN.getName(), SilverpeasRole.PUBLISHER.getName(),
            SilverpeasRole.WRITER.getName()};
    return checkUserRoles(componentId, userId, grantedRoles);
  }

  @Override
  public boolean isUserCanPublish(String componentId, String userId) {
    String[] grantedRoles =
        new String[]{SilverpeasRole.ADMIN.getName(), SilverpeasRole.PUBLISHER.getName()};
    return checkUserRoles(componentId, userId, grantedRoles);
  }

  private boolean checkUserRoles(String componentId, String userId, String... roles) {
    SilverpeasRole userProfile = SilverpeasRole.fromString(
        KmeliaHelper.getProfile(getUserRoles(componentId, userId)));
    boolean checked = Objects.requireNonNull(userProfile).isInRole(roles);

    if (!checked && isRightsOnTopicsEnabled(componentId)) {
      // check if current user is publisher or admin on at least one descendant
      Iterator<NodeDetail> descendants = nodeService.getDescendantDetails(getRootPK(componentId))
          .iterator();
      while (!checked && descendants.hasNext()) {
        NodeDetail descendant = descendants.next();
        if (descendant.haveLocalRights()) {
          // check if user is admin, publisher or writer on this topic
          String[] profiles = adminController.getProfilesByObjectAndUserId(
              ProfiledObjectId.fromNode(descendant.getNodePK().getId()), componentId, userId);
          if (profiles.length > 0) {
            userProfile = SilverpeasRole.fromString(KmeliaHelper.getProfile(profiles));
            checked = Objects.requireNonNull(userProfile).isInRole(roles);
          }
        }
      }
    }
    return checked;
  }

  @Override
  public NodeDetail getExpandedPathToNode(NodePK pk, String userId) {
    String instanceId = pk.getInstanceId();
    List<NodeDetail> nodes = new ArrayList<>(nodeService.getPath(pk));
    Collections.reverse(nodes);
    nodes.remove(0);

    List<NodeDetail> treeview = null;
    if (isNbItemsDisplayed(instanceId)) {
      treeview = getTreeview(getRootPK(instanceId), userId);
    }

    NodeDetail root = getRoot(instanceId, userId, treeview);

    // set nb objects in nodes
    if (treeview != null) {
      // set nb objects on root
      root.setNbObjects(treeview.get(0).getNbObjects());
      // set nb objects in each allowed nodes
      setNbItemsOfFolders(instanceId, nodes, treeview);
    }

    NodeDetail currentNode = root;
    for (NodeDetail node : nodes) {
      // get children of each node on path to target node
      Collection<NodeDetail> children = nodeService.getChildrenDetails(node.getNodePK());
      node.setChildrenDetails(children);
      setAllowedSubfolders(node, userId);
      if (treeview != null) {
        setNbItemsOfSubfolders(node, treeview, userId);
      }
      if (currentNode != null) {
        currentNode = find(currentNode.getChildrenDetails(), node);
      }
      if (currentNode != null) {
        currentNode.setChildrenDetails(node.getChildrenDetails());
      }
    }
    return root;

  }

  private NodeDetail find(Collection<NodeDetail> nodes, NodeDetail toFind) {
    for (NodeDetail node : nodes) {
      if (node.getNodePK().getId().equals(toFind.getNodePK().getId())) {
        return node;
      }
    }
    return null;
  }

  /**
   * Removes publications according to given ids. Before a publication is removed, user privileges
   * are controlled. If node defines the trash, publications are definitively deleted. Otherwise,
   * publications move into trash.
   * @param publiIds the ids of publications to delete
   * @param topicId the node where the publications are
   * @param userId the user who wants to perform deletion
   * @return the list of publication ids which has been really deleted
   * @
   */
  @Override
  @Transactional(Transactional.TxType.REQUIRED)
  public List<String> deletePublications(List<String> publiIds, NodePK topicId, String userId) {
    List<String> removedIds = new ArrayList<>();
    String profile = getProfile(userId, topicId);
    for (String id : publiIds) {
      PublicationPK pk = new PublicationPK(id, topicId);
      if (isUserCanDeletePublication(new PublicationPK(id, topicId), profile, userId)) {
        try {
          if (topicId.isTrash() && isPublicationInBasket(pk)) {
            deletePublication(pk);
          } else {
            sendPublicationInBasket(pk);
          }
          removedIds.add(id);
        } catch (Exception e) {
          SilverLogger.getLogger(this)
              .error("Deletion of publication {0} failed", new String[]{pk.getId()}, e);
        }
      }
    }
    return removedIds;
  }

  @Override
  @Transactional(Transactional.TxType.REQUIRED)
  public void deletePublication(PublicationPK pubPK, String userId) {
    NodePK topicId = publicationService.getMainLocation(pubPK)
        .orElse(null);
    String profile = topicId != null ? getProfile(userId, topicId) : null;
    if (profile != null && isUserCanDeletePublication(pubPK, profile, userId)) {
      try {
        if (topicId.isTrash()) {
          deletePublication(pubPK);
        } else {
          sendPublicationInBasket(pubPK);
        }
      } catch (Exception e) {
        SilverLogger.getLogger(this)
            .error("Deletion of publication {0} failed", new String[]{pubPK.getId()}, e);
      }
    }
  }

  @Transactional(Transactional.TxType.REQUIRED)
  @Override
  public void deleteTopic(@NonNull NodePK topic, String userId) {
    Objects.requireNonNull(topic);
    if (topic.isTrash() || topic.isRoot()) {
      return;
    }
    NodeDetail folder = getNodeHeader(topic);
    boolean isInTrash = isInTrash(topic);
    // check if user is allowed to delete this topic
    NodePK root = new NodePK(NodePK.ROOT_NODE_ID, topic.getInstanceId());
    if (SilverpeasRole.ADMIN.isInRole(getUserTopicProfile(topic, userId)) ||
        SilverpeasRole.ADMIN.isInRole(getUserTopicProfile(root, userId)) ||
        SilverpeasRole.ADMIN.isInRole(getUserTopicProfile(folder.getFatherPK(), userId))) {
      if (isInTrash) {
        deleteTopic(topic);
      } else {
        sendTopicInBasket(folder);
      }
    }
  }

  private boolean isUserCanDeletePublication(PublicationPK pubPK, String profile, String userId) {
    User owner = getPublication(pubPK).getCreator();
    return KmeliaPublicationHelper.isRemovable(pubPK.getInstanceId(), userId, profile, owner);
  }

  @Override
  public Optional<KmeliaPublication> getContributionById(ContributionIdentifier contributionId) {
    return Optional.of(getPublication(
        new PublicationPK(contributionId.getLocalId(), contributionId.getComponentInstanceId())));
  }

  @Override
  public SettingBundle getComponentSettings() {
    return settings;
  }

  @Override
  public LocalizationBundle getComponentMessages(String language) {
    return ResourceLocator.getLocalizationBundle(MESSAGES_PATH, language);
  }

  @Override
  public boolean isRelatedTo(final String instanceId) {
    return instanceId.startsWith("kmelia") || instanceId.startsWith("kmax") ||
        instanceId.startsWith("toolbox");
  }

  private void removeOrRestoreNode(NodeDetail node, boolean isRemoval) {
    if (node.isRemoved() && !isRemoval) {
      nodeService.restoreNode(node);
    } else if (!node.isRemoved() && isRemoval) {
      nodeService.removeNode(node);
    }
  }

  private boolean isInTrash(NodePK nodePK) {
    if (nodePK.isTrash()) {
      return true;
    } else if (nodePK.isRoot() || nodePK.isUnclassed()) {
      return false;
    }
    var destinationPath = nodeService.getPath(nodePK);
    return destinationPath.stream().anyMatch(NodeDetail::isBin);
  }

  @SimulationActionProcess(elementLister = KmeliaNodeSimulationElementLister.class)
  @Action(ActionType.MOVE)
  @Transactional
  @Override
  public void moveNode(@SourcePK NodePK nodePK, @TargetPK NodePK to,
      KmeliaPasteDetail pasteContext) {
    List<NodeDetail> treeToPaste = nodeService.getSubTree(nodePK);
    boolean inTrash = isInTrash(to);
    boolean movedToAnotherApp = !nodePK.getInstanceId().equals(to.getInstanceId());
    boolean rightsOnTopicsEnabled = isRightsOnTopicsEnabled(to.getInstanceId());

    // move node and subtree
    nodeService.moveNode(nodePK, to, rightsOnTopicsEnabled);

    for (NodeDetail fromNode : treeToPaste) {
      if (fromNode != null) {
        NodePK toNodePK = new NodePK(fromNode.getNodePK().getId(), to);
        NodeDetail toNode;
        if (movedToAnotherApp) {
          toNode = nodeService.getDetail(toNodePK);
          updateNodeRights(toNode, fromNode, rightsOnTopicsEnabled);
          // move rich description of node
          if (!nodePK.getInstanceId().equals(to.getInstanceId())) {
            WysiwygController.move(fromNode.getNodePK().getInstanceId(),
                NODE_PREFIX + fromNode.getId(), to.getInstanceId(), NODE_PREFIX + toNodePK.getId());
          }
        } else {
          toNode = fromNode;
        }
        removeOrRestoreNode(toNode, inTrash);

        // move publications of node
        movePublicationsOfTopic(fromNode.getNodePK(), toNodePK, pasteContext);
      }
    }

    nodePK.setComponentName(to.getInstanceId());
  }

  private void updateNodeRights(final NodeDetail node, final NodeDetail fromNode,
      final boolean rightsOnTopicsEnabled) {
    if (fromNode.haveLocalRights()) {
      List<ProfileInst> profiles = adminController.getProfilesByObject(
          ProfiledObjectId.fromNode(fromNode.getNodePK().getId()),
          fromNode.getNodePK().getInstanceId());
      if (rightsOnTopicsEnabled) {
        // adjusting previous rights according to target component after deleting the previous
        // existing profiles
        filterTopicProfiles(profiles, node, null,
            p -> adminController.deleteProfileInst(p.getId()));
      } else {
        // target component does not use specific rights, so removing rights
        for (ProfileInst profile : profiles) {
          adminController.deleteProfileInst(profile.getId());
        }
      }
    }
  }

  private void movePublicationsOfTopic(NodePK fromPK, NodePK toPK, KmeliaPasteDetail pasteContext) {
    Collection<PublicationDetail> publications = publicationService.getDetailsByFatherPK(fromPK);
    pasteContext.setFromPK(fromPK);
    for (PublicationDetail publi : publications) {
      movePublication(publi.getPK(), toPK, pasteContext);
    }
  }

  @SimulationActionProcess(elementLister = KmeliaNodeSimulationElementLister.class)
  @Action(ActionType.COPY)
  @Override
  public NodeDetail copyNode(@SourcePK @TargetPK KmeliaCopyDetail copyDetail) {
    HashMap<String, String> oldAndNewIds = new HashMap<>();
    return copyNode(copyDetail, oldAndNewIds);
  }

  private NodeDetail copyNode(KmeliaCopyDetail copyDetail, HashMap<String, String> oldAndNewIds) {
    NodePK nodePKToCopy = copyDetail.getFromNodePK();
    NodePK targetPK = copyDetail.getToNodePK();
    String userId = copyDetail.getUserId();
    NodeDetail nodeToCopy = nodeService.getDetail(nodePKToCopy);
    NodeDetail father = getNodeHeader(targetPK);
    boolean rightsOnTopicsEnabled = isRightsOnTopicsEnabled(targetPK.getInstanceId());
    boolean inTrash = isInTrash(targetPK);

    // paste topic
    NodePK nodePK = new NodePK(UNKNOWN, targetPK);
    NodeDetail node = new NodeDetail(nodeToCopy);
    node.setNodePK(nodePK);
    node.setCreatorId(userId);
    node.setRightsDependsOn(NodeDetail.NO_RIGHTS_DEPENDENCY);
    node.setCreationDate(new Date());
    if (inTrash) {
      node.setRemovalStatus(new Date(), userId);
    } else {
      node.setRemovalStatus(null, null);
    }
    NodeDetail copiedNode = nodeService.createNode(node, father);

    // duplicate the predefined classification on the PdC if any
    copyNodePredefinedClassification(nodeToCopy.getNodePK(), copiedNode.getNodePK());

    // duplicate rights
    if (rightsOnTopicsEnabled && copyDetail.isNodeRightsMustBeCopied()) {
      oldAndNewIds.put(nodePKToCopy.getId(), copiedNode.getId());
      setNodeRightsDependency(nodeToCopy, father, copiedNode);
      // Set topic rights if any
      copyNodeRights(userId, nodeToCopy, copiedNode);
    }

    // paste wysiwyg attached to node
    WysiwygController.copy(nodePKToCopy.getInstanceId(), NODE_PREFIX + nodePKToCopy.getId(),
        copiedNode.getNodePK().getInstanceId(), NODE_PREFIX + copiedNode.getId(), userId);

    // associate model used by copied folder to new folder
    copyUsedModel(nodePKToCopy, copiedNode.getNodePK());

    // paste publications of topics
    KmeliaCopyDetail folderContentCopy = new KmeliaCopyDetail(copyDetail);
    folderContentCopy.setFromNodePK(nodePKToCopy);
    folderContentCopy.setToNodePK(copiedNode.getNodePK());

    if (copyDetail.isPublicationHeaderMustBeCopied()) {
      copyPublications(folderContentCopy);
    }

    // paste subtopics
    Collection<NodeDetail> subtopics = nodeToCopy.getChildrenDetails();
    for (NodeDetail subTopic : subtopics) {
      if (subTopic != null) {
        folderContentCopy.setFromNodePK(subTopic.getNodePK());
        copyNode(folderContentCopy, oldAndNewIds);
      }
    }

    return copiedNode;
  }

  private void copyNodeRights(final String userId, final NodeDetail sourceNode,
      final NodeDetail targetNode) {
    if (sourceNode.haveLocalRights()) {
      List<ProfileInst> topicProfiles = adminController.getProfilesByObject(
          ProfiledObjectId.fromNode(sourceNode.getNodePK().getId()),
          sourceNode.getNodePK().getInstanceId());
      filterTopicProfiles(topicProfiles, targetNode, userId, p -> {
      });
    }
  }

  private void copyNodePredefinedClassification(final NodePK nodeToCopy, final NodePK nodePK) {
    PdcClassification nodeClassification =
        pdcClassificationService.getPreDefinedClassification(nodeToCopy.getId(),
            nodeToCopy.getComponentInstanceId());
    if (!nodeClassification.isEmpty() && nodeClassification.isPredefinedForANode()) {
      PdcClassification copy = nodeClassification.copy()
          .forNode(nodePK.getId())
          .inComponentInstance(nodePK.getComponentInstanceId());
      pdcClassificationService.savePreDefinedClassification(copy);
    }
  }

  private void filterTopicProfiles(List<ProfileInst> profiles, NodeDetail node, String userId,
      Consumer<ProfileInst> preFiltering) {
    NodeDetail father = getNodeHeader(node.getFatherPK());
    List<ProfileInst> fatherProfiles = new ArrayList<>();
    if (father.haveRights()) {
      fatherProfiles.addAll(
          adminController.getProfilesByObject(ProfiledObjectId.fromNode(father.getRightsDependsOn()),
              father.getNodePK().getInstanceId()));
    }
    for (ProfileInst topicProfile : profiles) {
      preFiltering.accept(topicProfile);
      ProfileInst nodeProfileInst = new ProfileInst(topicProfile);
      filterUsersAndGroupsInNodeProfile(nodeProfileInst, fatherProfiles, node);
      // Add the profile
      adminController.addProfileInst(nodeProfileInst, userId);
    }
  }

  /*
   * Filter the users and the groups defined in the given profile according to the profiles
   * of either the topic parent or component instance. It removes all groups and users which are
   * not authorized to access the parent topic or the component instance.
   */
  private void filterUsersAndGroupsInNodeProfile(ProfileInst profile,
      List<ProfileInst> fatherProfiles, NodeDetail node) {
    List<String> verifiedUserIds = new ArrayList<>();
    List<String> verifiedGroupIds = new ArrayList<>();
    String instanceId = node.getNodePK().getInstanceId();

    if (!fatherProfiles.isEmpty()) {
      // check users and groups according to a father node rights
      fatherProfiles.stream()
          .flatMap(p -> p.getAllUsers().stream())
          .distinct()
          .filter(u -> profile.getAllUsers().contains(u))
          .forEach(verifiedUserIds::add);

      fatherProfiles.stream()
          .flatMap(p -> p.getAllGroups().stream())
          .distinct()
          .filter(g -> profile.getAllGroups().contains(g))
          .forEach(verifiedGroupIds::add);
    } else {
      // check users and groups according to component instance rights
      ComponentAccessControl accessControl = ComponentAccessControl.get();
      profile.getAllUsers().stream()
          .filter(u -> accessControl.isUserAuthorized(u, instanceId))
          .forEach(verifiedUserIds::add);
      profile.getAllGroups().stream()
          .filter(g -> accessControl.isGroupAuthorized(g, instanceId))
          .forEach(verifiedGroupIds::add);
    }

    profile.setId("-1");
    profile.setUsers(verifiedUserIds);
    profile.setGroups(verifiedGroupIds);
    profile.setComponentFatherId(getComponentLocalId(instanceId));
    profile.setObjectId(ProfiledObjectId.fromNode(node.getId()));
    profile.setParentObjectId(ProfiledObjectId.fromNode(node.getFatherPK().getId()));
  }

  private void setNodeRightsDependency(final NodeDetail nodeToCopy, final NodeDetail father,
      final NodeDetail node) {
    if (nodeToCopy.haveRights()) {
      if (nodeToCopy.haveLocalRights()) {
        node.setRightsDependsOnMe();
      } else {
        node.setRightsDependsOn(father.getRightsDependsOn());
      }
      nodeService.updateRightsDependency(node);
    }
  }

  @SimulationActionProcess(elementLister = KmeliaPublicationSimulationElementLister.class)
  @Action(ActionType.COPY)
  @Override
  @Transactional(Transactional.TxType.REQUIRED)
  public void copyPublications(@SourcePK @TargetPK KmeliaCopyDetail copyDetail) {
    Collection<PublicationDetail> publications =
        publicationService.getDetailsByFatherPK(copyDetail.getFromNodePK());
    for (PublicationDetail publi : publications) {
      copyPublication(publi, copyDetail);
    }
  }

  @SimulationActionProcess(elementLister = KmeliaPublicationSimulationElementLister.class)
  @Action(ActionType.COPY)
  @Override
  @Transactional(Transactional.TxType.REQUIRED)
  public PublicationDetail copyPublication(@SourcePK PublicationDetail publiToCopy,
      @TargetPK KmeliaCopyDetail copyDetail) {
    NodePK toNodePK = copyDetail.getToNodePK();
    String userId = copyDetail.getUserId();
    final String toComponentId = toNodePK.getInstanceId();
    final NodePK fromNodePK = copyDetail.getFromNodePK();
    final KmeliaPublication publication = fromDetail(publiToCopy, fromNodePK);
    try {
      if (publication.isAlias()) {
        Location location = new Location(copyDetail.getToNodePK().getId(), toComponentId);
        location.setAsAlias(userId);
        publicationService.addAliases(publiToCopy.getPK(), singletonList(location));
        return publiToCopy;
      } else {
        return copyPublication(publiToCopy, toNodePK, toComponentId, copyDetail, userId);
      }
    } catch (Exception ex) {
      SilverLogger.getLogger(this).error("Publication copy failure", ex);
    }
    return null;
  }

  private PublicationDetail copyPublication(final PublicationDetail publiToCopy,
      final NodePK toNodePK,
      final String toComponentId, final KmeliaCopyDetail copyDetail, final String userId)
      throws PdcException, PublicationTemplateException, FormException {
    ResourceReference toResourceReference = new ResourceReference(ResourceReference.UNKNOWN_ID,
        toNodePK.getInstanceId());
    PublicationPK toPubPK = new PublicationPK(UNKNOWN, toNodePK);

    // Handle duplication as a creation, ignore initial parameters
    PublicationDetail newPubli = PublicationDetail.builder(publiToCopy.getLanguage())
        .setPk(toPubPK)
        .setNameAndDescription(publiToCopy.getName(), publiToCopy.getDescription())
        .setVersion(publiToCopy.getVersion())
        .setKeywords(publiToCopy.getKeywords())
        .setBeginDateTime(publiToCopy.getBeginDate(), publiToCopy.getBeginHour())
        .setEndDateTime(publiToCopy.getEndDate(), publiToCopy.getEndHour())
        .setImportance(publiToCopy.getImportance())
        .build();

    newPubli.setTranslations(publiToCopy.getClonedTranslations());
    newPubli.setAuthor(publiToCopy.getAuthor());
    newPubli.setCreatorId(userId);
    newPubli.setCreationDate(new Date());
    if (copyDetail.isPublicationContentMustBeCopied()) {
      newPubli.setInfoId(publiToCopy.getInfoId());
    }

    if (copyDetail.isAdministrativeOperation()) {
      newPubli.setCreatorId(publiToCopy.getCreatorId());
      newPubli.setCreationDate(publiToCopy.getCreationDate());
      newPubli.setUpdaterId(publiToCopy.getUpdaterId());
      newPubli.setUpdateDate(publiToCopy.getLastUpdateDate());
      newPubli.setStatus(publiToCopy.getStatus());
      newPubli.setTargetValidatorId(publiToCopy.getTargetValidatorId());
    } else {
      // use validators selected via UI
      newPubli.setTargetValidatorId(copyDetail.getPublicationValidatorIds());

      // manage status explicitly to bypass Draft mode
      setToByPassDraftMode(copyDetail, toNodePK, newPubli, userId);
    }

    String fromId = publiToCopy.getPK().getId();
    String fromComponentId = publiToCopy.getPK().getInstanceId();
    ResourceReference fromResourceReference = new ResourceReference(publiToCopy.getPK().getId(),
        fromComponentId);
    PublicationPK fromPubPK = new PublicationPK(publiToCopy.getPK().getId(), fromComponentId);

    String id = createPublicationIntoTopic(newPubli, toNodePK);
    // update id cause new publication is created
    toPubPK.setId(id);
    toResourceReference.setId(id);

    // Copy vignette
    ThumbnailController.copyThumbnail(fromResourceReference, toResourceReference);

    // Copy positions on Pdc
    if (copyDetail.isPublicationPositionsMustBeCopied()) {
      copyPdcPositions(fromPubPK, toPubPK);
    }

    // Copy files
    Map<String, String> fileIds = new HashMap<>();
    if (copyDetail.isPublicationFilesMustBeCopied()) {
      fileIds.putAll(copyFiles(fromPubPK, toPubPK));
    }

    // Copy content
    if (copyDetail.isPublicationContentMustBeCopied()) {
      String xmlFormShortName = newPubli.getInfoId();
      if (xmlFormShortName != null && !"0".equals(xmlFormShortName)) {
        registerXmlForm(fromComponentId, fromResourceReference, toComponentId, toResourceReference,
            xmlFormShortName, fileIds);
      } else {
        // paste wysiwyg
        WysiwygController.copy(fromComponentId, fromId, toPubPK.getInstanceId(), id, userId);
      }
    }

    // Index publication to index its files and content
    publicationService.createIndex(toPubPK);

    return newPubli;
  }

  private void setToByPassDraftMode(@TargetPK final KmeliaCopyDetail copyDetail,
      final NodePK nodePK, final PublicationDetail newPubli, final String userId) {
    if (StringUtil.isDefined(copyDetail.getPublicationStatus())) {
      String profile = getProfile(userId, nodePK);
      if (!copyDetail.getPublicationStatus().equals(PublicationDetail.DRAFT_STATUS)) {
        //noinspection ConstantConditions
        if (SilverpeasRole.fromString(profile).isGreaterThanOrEquals(SilverpeasRole.PUBLISHER)) {
          newPubli.setStatus(PublicationDetail.VALID_STATUS);
        } else {
          // case of writer
          newPubli.setStatus(PublicationDetail.TO_VALIDATE_STATUS);
        }
      }
    }
  }

  private void registerXmlForm(final String fromComponentId,
      final ResourceReference fromResourceReference, final String toComponentId,
      final ResourceReference toResourceReference, final String xmlFormShortName,
      final Map<String, String> fileIds) throws PublicationTemplateException, FormException {
    // Content = XMLForm
    // register xmlForm to publication
    PublicationTemplateManager publicationTemplateManager =
        PublicationTemplateManager.getInstance();
    GenericRecordSet toRecordset = publicationTemplateManager.addDynamicPublicationTemplate(
        toComponentId + ":" + xmlFormShortName, xmlFormShortName + ".xml");

    PublicationTemplate pubTemplate =
        publicationTemplateManager.getPublicationTemplate(fromComponentId + ":" + xmlFormShortName);
    RecordSet set = pubTemplate.getRecordSet();

    set.copy(fromResourceReference, toResourceReference, toRecordset.getRecordTemplate(), fileIds);
  }

  private Map<String, String> copyFiles(PublicationPK fromPK, PublicationPK toPK) {
    Map<String, String> fileIds = new HashMap<>();
    List<SimpleDocument> origins =
        getAttachmentService().listDocumentsByForeignKeyAndType(fromPK.toResourceReference(),
            DocumentType.attachment, null);
    for (SimpleDocument origin : origins) {
      SimpleDocumentPK copyPk =
          getAttachmentService().copyDocument(origin, new ResourceReference(toPK));
      fileIds.put(origin.getId(), copyPk.getId());
    }
    return fileIds;
  }

  private void copyPdcPositions(PublicationPK fromPK, PublicationPK toPK) throws PdcException {
    int fromSilverObjectId = getSilverObjectId(fromPK);
    int toSilverObjectId = getSilverObjectId(toPK);

    pdcManager.copyPositions(fromSilverObjectId, fromPK.getInstanceId(), toSilverObjectId,
        toPK.getInstanceId());
  }

  @Override
  public List<KmeliaPublication> filterPublications(List<KmeliaPublication> publications,
      String instanceId, SilverpeasRole profile, String userId) {
    final boolean coWriting = isCoWritingEnable(instanceId);
    final RemovedSpaceAndComponentInstanceChecker checker =
        RemovedSpaceAndComponentInstanceChecker.create();
    final Predicate<KmeliaPublication> removedComponentInstance =
        k -> checker.isRemovedComponentInstanceById(k.getComponentInstanceId());
    final Predicate<KmeliaPublication> visiblePublication =
        k -> isPublicationVisible(k.getDetail(), profile, userId, coWriting);
    return publications.stream()
        .filter(not(removedComponentInstance).and(visiblePublication))
        .collect(Collectors.toList());
  }

  @Override
  public void userHaveBeenDeleted(String userId) {
    List<PublicationDetail> publications = publicationService.removeUserFromTargetValidators(
        userId);
    SilverLogger.getLogger(this)
        .debug("User ''{0}'' have been removed from {1} publications as target validator", userId,
            publications.size());

    // Validation process is performed, maybe some must be validated.
    KmeliaValidation.by(userId).validatorHasNoMoreRight().validate(publications);
  }

  private boolean isPublicationVisible(PublicationDetail detail, SilverpeasRole profile,
      String userId, boolean coWriting) {
    if (detail.getStatus() != null) {
      if (detail.isValid()) {
        return isVisible(detail, profile, userId, coWriting);
      } else {
        // si le thème est en co-rédaction, toutes les publications sont visibles par tous,
        // sauf les lecteurs
        if (detail.isDraft()) {
          // si le theme est en co-rédaction et si on autorise le mode brouillon visible par tous
          // toutes les publications en mode brouillon sont visibles par tous, sauf les lecteurs
          // sinon, seules les publications brouillon sont visibles à l'utilisateur
          return isVisibleInDraft(detail, profile, userId, coWriting);
        } else {
          return isVisibleInPublished(detail, profile, userId, coWriting);
        }
      }
    }
    return false;
  }

  private boolean isVisibleInDraft(final PublicationDetail detail, final SilverpeasRole profile,
      final String userId, final boolean coWriting) {
    return userId.equals(detail.getCreatorId()) || userId.equals(detail.getUpdaterId()) ||
        (coWriting && isDraftVisibleWithCoWriting() && profile != SilverpeasRole.USER) ||
        (detail.haveGotClone() && profile.isGreaterThanOrEquals(SilverpeasRole.PUBLISHER));
  }

  private boolean isVisibleInPublished(final PublicationDetail detail, final SilverpeasRole profile,
      final String userId, final boolean coWriting) {
    return profile == SilverpeasRole.ADMIN || profile == SilverpeasRole.PUBLISHER ||
        userId.equals(detail.getCreatorId()) || userId.equals(detail.getUpdaterId()) ||
        (profile != SilverpeasRole.USER && coWriting);
  }

  private boolean isVisible(final PublicationDetail detail, final SilverpeasRole profile,
      final String userId, final boolean coWriting) {
    if (detail.isVisible()) {
      return true;
    } else {
      return profile == SilverpeasRole.ADMIN || userId.equals(detail.getUpdaterId()) ||
          (profile != SilverpeasRole.USER && coWriting);
    }
  }

  private PublicationDetail clonePublication(String cloneId, PublicationPK pubPK,
      String validatorUserId, Date validationDate) {
    PublicationPK tempPK = new PublicationPK(cloneId, pubPK);
    CompletePublication publication = publicationService.getCompletePublication(tempPK);
    PublicationDetail clone = getClone(publication.getPublicationDetail());
    clone.setPk(pubPK);
    if (validatorUserId != null) {
      clone.setValidatorId(validatorUserId);
      clone.setValidateDate(validationDate != null ? validationDate : new Date());
    }
    clone.setStatus(PublicationDetail.VALID_STATUS);
    clone.setCloneId(UNDEFINED_NODE_ID);
    clone.setCloneStatus(null);
    return clone;
  }

  private RecordSet getXMLFormFrom(String infoId, PublicationPK pubPK)
      throws PublicationTemplateException {
    PublicationTemplateManager publicationTemplateManager =
        PublicationTemplateManager.getInstance();
    PublicationTemplate pubTemplate = publicationTemplateManager.getPublicationTemplate(
        pubPK.getInstanceId() + ":" + infoId);

    return pubTemplate.getRecordSet();
  }

  protected void onDocumentDeletion(AttachmentRef attachment) {
    Optional<KmeliaOperationContext> context = KmeliaOperationContext.current();
    if (context.isEmpty() || !context.get().isAbout(DELETION)) {
      PublicationPK pubPK = new PublicationPK(attachment.getForeignId(),
          attachment.getInstanceId());
      externalElementsOfPublicationHaveChanged(pubPK, attachment.getUserId(), false);
    }
  }

  @Override
  public UserNotification getUserNotification(NodePK pk) {
    NodeDetail node = getNodeHeader(pk);
    return new KmeliaNotifyTopicUserNotification(node).build();
  }

  @Override
  public List<String> getActiveValidatorIds(PublicationPK pk) {
    PublicationDetail pub = getPublicationDetail(pk);
    return getActiveValidatorIds(pub);
  }

  private List<String> getActiveValidatorIds(PublicationDetail publication) {
    List<String> activeValidatorIds = new ArrayList<>();
    String[] validatorIds = publication.getTargetValidatorIds();
    if (validatorIds == null) {
      return activeValidatorIds;
    }
    for (String userId : validatorIds) {
      String profile = getProfileOnPublication(userId, publication.getPK());
      //noinspection ConstantConditions
      if (profile != null &&
          SilverpeasRole.fromString(profile).isGreaterThanOrEquals(SilverpeasRole.PUBLISHER)) {
        activeValidatorIds.add(userId);
      }
    }
    return activeValidatorIds;
  }

  @Override
  public void performReminder(final Reminder reminder) {
    if (KMELIA_DELAYED_VISIBILITY_USER_NOTIFICATION.asString().equals(reminder.getProcessName())) {
      getContributionById(reminder.getContributionId()).filter(
              p -> !isPublicationInBasket(p.getPk()))
          .map(KmeliaPublication::getDetail)
          .ifPresent(p -> sendSubscriptionsNotification(p, NotifAction.PUBLISHED, false));
    }
  }

  @Override
  @Transactional(Transactional.TxType.REQUIRED)
  public void deleteClone(PublicationPK pk) {
    PublicationDetail clone = getPublicationDetail(pk);
    PublicationDetail original = getPublicationDetail(clone.getClonePK());

    //delete clone itself
    deletePublication(pk);

    //remove reference to clone from original publication
    original.setCloneId(null);
    original.setCloneStatus(null);
    original.setUpdateDataMustBeSet(false);
    original.setIndexOperation(IndexManager.NONE);
    publicationService.setDetail(original);
  }

  @Override
  public List<KmeliaPublication> getNonVisiblePublications(String componentId, String userId) {
    try {
      Collection<PublicationDetail> temp = publicationService.getPublicationsByCriteria(
          PublicationCriteria.onComponentInstanceIds(componentId)
              .ofStatus(PublicationDetail.VALID_STATUS)
              .nonVisibleAt(OffsetDateTime.now())
              .excludingNodes(NodePK.BIN_NODE_ID)
              .orderByDescendingLastUpdateDate());
      // only publications allowed by current user must be returned
      List<PublicationDetail> publications = PublicationAccessControl.get()
          .filterAuthorizedByUser(userId, temp)
          .collect(Collectors.toList());
      return asKmeliaPublication(publications);
    } catch (Exception e) {
      throw new KmeliaRuntimeException(e);
    }
  }

  private class PublicationConcernedByUpdate {
    private final PublicationPK pubPK;
    private PublicationDetail pubDetail;
    private boolean isPublicationInBasketBeforeUpdate;

    public PublicationConcernedByUpdate(final PublicationPK pubPK) {
      this.pubPK = pubPK;
    }

    public PublicationDetail getPubDetail() {
      return pubDetail;
    }

    public boolean isPublicationInBasketBeforeUpdate() {
      return isPublicationInBasketBeforeUpdate;
    }

    public PublicationConcernedByUpdate invoke() {
      pubDetail = null;
      isPublicationInBasketBeforeUpdate = false;
      try {
        isPublicationInBasketBeforeUpdate = isPublicationInBasket(pubPK);
        pubDetail = getPublicationDetail(pubPK);
      } catch (Exception e) {
        // publication no longer exists do not throw exception because this method is called by JMS
        // layer
        // if exception is throw, JMS will attempt to execute it again and again...
        SilverLogger.getLogger(DefaultKmeliaService.this)
            .error("Impossible to get the publication {0}", pubPK.getId());
      }
      return this;
    }

    public boolean isPublicationNotDefined() {
      return pubDetail == null || (StringUtil.isDefined(pubPK.getInstanceId()) &&
          !pubDetail.getInstanceId().equals(pubPK.getInstanceId()));
    }
  }

  private class ValidationChecker {
    private PublicationPK pubPK;
    private String userId;
    private PublicationDetail currentPubDetail;
    private PublicationDetail currentPubOrCloneDetail;
    private PublicationPK validatedPK;
    private boolean validationComplete = false;
    private String validatorUserId;
    private Date validationDate;

    public ValidationChecker setPubPK(final PublicationPK pubPK) {
      this.pubPK = pubPK;
      return this;
    }

    public ValidationChecker setUserId(final String userId) {
      this.userId = userId;
      return this;
    }

    public ValidationChecker setCurrentPubDetail(final PublicationDetail currentPubDetail) {
      this.currentPubDetail = currentPubDetail;
      return this;
    }

    public ValidationChecker setCurrentPubOrCloneDetail(
        final PublicationDetail currentPubOrCloneDetail) {
      this.currentPubOrCloneDetail = currentPubOrCloneDetail;
      return this;
    }

    public ValidationChecker setValidatedPK(final PublicationPK validatedPK) {
      this.validatedPK = validatedPK;
      return this;
    }

    public ValidationChecker setValidatorUserId(final String validatorUserId) {
      this.validatorUserId = validatorUserId;
      return this;
    }

    public ValidationChecker setValidationDate(final Date validationDate) {
      this.validationDate = validationDate;
      return this;
    }

    public boolean isValidationComplete() {
      return validationComplete;
    }

    public String getValidatorUserId() {
      return validatorUserId;
    }

    public Date getValidationDate() {
      return validationDate;
    }

    public ValidationChecker check() {
      // User has no more validation right
      final int validationType = getValidationType(pubPK.getInstanceId());
      boolean alertPublicationOwnerThereIsNoMoreValidator = false;
      switch (validationType) {
        case KmeliaHelper.VALIDATION_CLASSIC:
        case KmeliaHelper.VALIDATION_TARGET_1:
          alertPublicationOwnerThereIsNoMoreValidator = true;
          break;
        default:
          // get all users who have to validate
          List<String> allValidators = getAllValidators(validatedPK);
          if (allValidators.isEmpty()) {
            alertPublicationOwnerThereIsNoMoreValidator = true;
          } else {
            // check if all validators have give their decision
            validationComplete =
                DefaultKmeliaService.this.isValidationComplete(validatedPK, allValidators);
            if (validationComplete) {
              findLastValidation();
            } else if (validationType == KmeliaHelper.VALIDATION_TARGET_N &&
                StringUtil.isNotDefined(currentPubOrCloneDetail.getTargetValidatorId())) {
              // Case of fallback solution when no more validator is defined, all publishers
              // must validate (as collegiate method)
              alertPublicationOwnerThereIsNoMoreValidator = true;
            }
          }
      }

      if (alertPublicationOwnerThereIsNoMoreValidator) {
        NodePK fatherPK = getPublicationFatherPK(currentPubDetail.getPK());
        if (fatherPK != null) {
          sendNoMoreValidatorNotification(fatherPK, currentPubDetail);
        }
      }
      return this;
    }

    private void findLastValidation() {
      // taking the last effective validator for the state change.
      validationDate = new Date(0);
      for (ValidationStep validationStep : publicationService.getValidationSteps(validatedPK)) {
        final String validationStepUserId = validationStep.getUserId();
        if (!validationStepUserId.equals(userId) &&
            validationStep.getValidationDate().compareTo(validationDate) > 0) {
          validationDate = validationStep.getValidationDate();
          validatorUserId = validationStepUserId;
        }
      }
    }

    private void sendNoMoreValidatorNotification(final NodePK fatherPK,
        final PublicationDetail pubDetail) {
      if (pubDetail.isValidationRequired() || pubDetail.isValid()) {
        try {
          UserNotificationHelper.buildAndSend(
              new KmeliaNoMoreValidatorPublicationUserNotification(fatherPK, pubDetail));
        } catch (Exception e) {
          SilverLogger.getLogger(this)
              .error("fatherId = {0}, pubPK = {1}",
                  new String[]{fatherPK.getId(), pubDetail.getPK().toString()}, e);
        }
      }
    }
  }
}
