CmisKmeliaContributionsProvider.java

/*
 * Copyright (C) 2000 - 2024 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception. You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "https://www.silverpeas.org/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.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisPermissionDeniedException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisServiceUnavailableException;
import org.silverpeas.components.kmelia.KmeliaPublicationHelper;
import org.silverpeas.components.kmelia.model.KmeliaPublication;
import org.silverpeas.components.kmelia.model.KmeliaRuntimeException;
import org.silverpeas.core.ResourceIdentifier;
import org.silverpeas.core.admin.component.model.SilverpeasSharedComponentInstance;
import org.silverpeas.core.admin.service.OrganizationController;
import org.silverpeas.core.admin.user.model.User;
import org.silverpeas.core.annotation.Service;
import org.silverpeas.core.cmis.CmisContributionsProvider;
import org.silverpeas.core.contribution.model.ContributionIdentifier;
import org.silverpeas.core.contribution.model.CoreContributionType;
import org.silverpeas.core.contribution.model.I18nContribution;
import org.silverpeas.core.contribution.publication.model.PublicationDetail;
import org.silverpeas.core.contribution.publication.model.PublicationPK;
import org.silverpeas.core.node.model.NodeDetail;
import org.silverpeas.core.node.model.NodePK;
import org.silverpeas.core.security.authorization.AccessControlContext;
import org.silverpeas.core.security.authorization.AccessControlOperation;
import org.silverpeas.core.security.authorization.PublicationAccessControl;

import javax.inject.Inject;
import javax.inject.Named;
import javax.transaction.Transactional;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.function.Predicate.not;

/**
 * Providers of the user contributions managed in a Kmelia instance and to expose in the CMIS
 * objects tree of Silverpeas.
 * @author mmoquillon
 */
@Service
@Named("kmelia" + CmisContributionsProvider.Constants.NAME_SUFFIX)
public class CmisKmeliaContributionsProvider implements CmisContributionsProvider {

  @Inject
  private KmeliaService kmeliaService;

  @Inject
  private OrganizationController controller;

  @Inject
  private PublicationAccessControl accessControl;

  @Override
  public List<I18nContribution> getAllowedRootContributions(final ResourceIdentifier appId,
      final User user) {
    String kmeliaId = appId.asString();
    checkKmeliaAccessible(kmeliaId, user);
    boolean treeEnabled = KmeliaPublicationHelper.isTreeEnabled(kmeliaId);
    NodePK root = new NodePK(NodePK.ROOT_NODE_ID, kmeliaId);
    if (treeEnabled) {
      ContributionIdentifier rootId = ContributionIdentifier.from(root, NodeDetail.TYPE);
      return getAllowedContributionsInFolder(rootId, user);
    } else {
      String profile = kmeliaService.getUserTopicProfile(root, user.getId());
      return kmeliaService.getAuthorizedPublicationsOfFolder(root, profile, user.getId(), false)
          .stream()
          .map(KmeliaPublication::getDetail)
          .collect(Collectors.toList());
    }
  }

  @Override
  public List<I18nContribution> getAllowedContributionsInFolder(final ContributionIdentifier folder,
      final User user) {
    String kmeliaId = folder.getComponentInstanceId();
    checkKmeliaAccessible(kmeliaId, user);
    boolean treeEnabled = KmeliaPublicationHelper.isTreeEnabled(kmeliaId);
    NodePK folderPK = toNodePK(folder);
    try {
      String profile = kmeliaService.getUserTopicProfile(folderPK, user.getId());
      Stream<? extends I18nContribution> publications;
      if (!folderPK.isRoot() || KmeliaPublicationHelper.isPublicationsOnRootAllowed(kmeliaId)) {
        var pubsInFolder = kmeliaService.getAuthorizedPublicationsOfFolder(folderPK, profile, user.getId(), treeEnabled)
            .stream()
            .filter(not(KmeliaPublication::isAlias).and(KmeliaPublication::isVisible))
            .map(KmeliaPublication::getDetail)
            .collect(Collectors.toList());
        var drafts = accessControl.filterAuthorizedByUser(user.getId(),
                pubsInFolder.stream()
                    .filter(this::isInDraft)
                    .collect(Collectors.toList()), AccessControlContext.init()
                    .onOperationsOf(AccessControlOperation.MODIFICATION))
            .collect(Collectors.toMap(PublicationDetail::getId, p -> {
              PublicationPK pk = new PublicationPK(p.getCloneId(), p.getInstanceId());
              return kmeliaService.getPublicationDetail(pk);
            }));
        publications = pubsInFolder.stream()
            .map(p -> drafts.getOrDefault(p.getId(), p));
      } else {
        publications = Stream.empty();
      }

      var subFolders =
          kmeliaService.getFolderChildren(folderPK, user.getId())
              .stream()
              .filter(n -> !KmeliaHelper.isToValidateFolder(n.getNodePK()
                  .getId()))
              .filter(n -> !n.isUnclassified() && !n.isBin() && !n.getId()
                  .equals(KmeliaHelper.SPECIALFOLDER_NONVISIBLEPUBS));

      return Stream.concat(publications, subFolders)
          .collect(Collectors.toList());
    } catch (Exception e) {
      throw new CmisObjectNotFoundException(e.getMessage());
    }
  }

  @Override
  public I18nContribution getContribution(final ContributionIdentifier contributionId,
      final User user) {
    if (contributionId.getType().equals(NodeDetail.TYPE)) {
      return getNodeDetail(contributionId, user);
    } else if (contributionId.getType().equals(PublicationDetail.TYPE)) {
      return getPublicationDetail(contributionId, user);
    }
    throw new CmisNotSupportedException("Don't support such contribution types " +
        contributionId.getType());
  }

  @Override
  @Transactional
  public I18nContribution createContribution(final I18nContribution contribution,
      final ResourceIdentifier appId, final String language) {
    User user = User.getCurrentRequester();
    String kmeliaId = appId.asString();
    checkKmeliaAccessible(kmeliaId, user);

    ContributionIdentifier rootFolder =
        ContributionIdentifier.from(kmeliaId, NodePK.ROOT_NODE_ID, CoreContributionType.NODE);
    return createContributionInFolder(contribution, rootFolder, language);
  }

  @Override
  @Transactional
  public I18nContribution createContributionInFolder(final I18nContribution contribution,
      final ContributionIdentifier folder, final String language) {
    if (contribution.getIdentifier().getType().equals(PublicationDetail.TYPE)) {
      PublicationDetail publication = toPublicationDetail(contribution, language);
      String id = kmeliaService.createPublicationIntoTopic(publication, toNodePK(folder));
      PublicationDetail saved = publication.copy();
      saved.setPk(new PublicationPK(id, publication.getInstanceId()));
      return publication;
    }
    throw new CmisNotSupportedException(
        String.format("CMIS creation of %s isn't yet supported in Kmelia",
            contribution.getContributionType()));
  }

  private NodePK toNodePK(final ContributionIdentifier identifier) {
    return new NodePK(identifier.getLocalId(), identifier.getComponentInstanceId());
  }

  private PublicationPK toPublicationPK(final ContributionIdentifier identifier) {
    return new PublicationPK(identifier.getLocalId(), identifier.getComponentInstanceId());
  }

  private PublicationDetail toPublicationDetail(final I18nContribution contribution,
      final String language) {
    PublicationDetail publication;
    if (contribution instanceof PublicationDetail) {
      publication = (PublicationDetail) contribution;
    } else {
      PublicationPK pk = new PublicationPK(contribution.getIdentifier()
          .getLocalId(), contribution.getIdentifier()
          .getComponentInstanceId());
      publication = PublicationDetail.builder(language)
          .setPk(pk)
          .setNameAndDescription(contribution.getName(), contribution.getDescription())
          .setImportance(1)
          .created(contribution.getCreationDate(), contribution.getCreator()
              .getId())
          .updated(contribution.getLastUpdateDate(), contribution.getLastUpdater()
              .getId())
          .build();
    }
    return publication;
  }

  private void checkKmeliaAccessible(String kmeliaId, User user) {
    if (controller.getComponentInstance(kmeliaId)
        .filter(SilverpeasSharedComponentInstance.class::isInstance)
        .map(SilverpeasSharedComponentInstance.class::cast)
        .filter(i -> i.getName()
            .equals("kmelia") && i.canBeAccessedBy(user))
        .isEmpty()) {
      throw new CmisObjectNotFoundException(
          String.format("The application %s doesn't exist or is not accessible to user %s",
              kmeliaId, user.getId()));
    }
  }

  private boolean isInDraft(final PublicationDetail publication) {
    return publication.isValid() && publication.haveGotClone() && !publication.isClone();
  }

  private NodeDetail getNodeDetail(final ContributionIdentifier id, final User user) {
    NodeDetail node;
    try {
      node = kmeliaService.getNodeHeader(id.getLocalId(), id.getComponentInstanceId());
      if (node == null) {
        throw new KmeliaRuntimeException("");
      }
    } catch (Exception e) {
      throw new CmisObjectNotFoundException(String.format("Folder %s not found!", id.asString()));
    }

    if (node.isBin() || node.isUnclassified() || KmeliaHelper.isToValidateFolder(node.getId()) ||
        node.getId().equals(KmeliaHelper.SPECIALFOLDER_NONVISIBLEPUBS)) {
      throw new CmisPermissionDeniedException("Forbidden access to special folders!");
    }

    if (!node.canBeAccessedBy(user)) {
      throw new CmisPermissionDeniedException("Forbidden access to folder " + id.asString());
    }

    return node;
  }

  private PublicationDetail getPublicationDetail(final ContributionIdentifier id, final User user) {
    try {
      PublicationDetail publication = kmeliaService.getPublicationDetail(toPublicationPK(id));
      if (publication == null) {
        throw new CmisObjectNotFoundException(String.format("Publication %s not found!",
            id.asString()));
      }
      if (!publication.canBeAccessedBy(user)) {
        throw new CmisPermissionDeniedException("Forbidden access to publication " + id.asString());
      }
      if (isInDraft(publication) &&
        accessControl.isUserAuthorized(user.getId(), publication,
            AccessControlContext.init().onOperationsOf(AccessControlOperation.MODIFICATION))) {
        PublicationPK pk = new PublicationPK(publication.getCloneId(), publication.getInstanceId());
        return kmeliaService.getPublicationDetail(pk);
      }
      return publication;
    } catch (Exception e) {
      throw new CmisServiceUnavailableException(e.getMessage());
    }
  }
}