DefaultQuickInfoService.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.quickinfo.model;

import org.silverpeas.components.delegatednews.service.DelegatedNewsService;
import org.silverpeas.components.delegatednews.service.DelegatedNewsServiceProvider;
import org.silverpeas.components.quickinfo.NewsByStatus;
import org.silverpeas.components.quickinfo.QuickInfoComponentSettings;
import org.silverpeas.components.quickinfo.notification.NewsEventNotifier;
import org.silverpeas.components.quickinfo.notification.QuickInfoDelayedVisibilityUserNotificationReminder;
import org.silverpeas.components.quickinfo.notification.QuickInfoSubscriptionUserNotification;
import org.silverpeas.components.quickinfo.repository.NewsRepository;
import org.silverpeas.components.quickinfo.service.QuickInfoContentManager;
import org.silverpeas.components.quickinfo.service.QuickInfoDateComparatorDesc;
import org.silverpeas.core.ResourceReference;
import org.silverpeas.core.admin.component.model.PasteDetail;
import org.silverpeas.core.admin.service.OrganizationController;
import org.silverpeas.core.admin.service.OrganizationControllerProvider;
import org.silverpeas.core.annotation.Service;
import org.silverpeas.core.contribution.attachment.AttachmentServiceProvider;
import org.silverpeas.core.contribution.attachment.model.Attachments;
import org.silverpeas.core.contribution.attachment.model.DocumentType;
import org.silverpeas.core.contribution.attachment.model.SimpleDocumentPK;
import org.silverpeas.core.contribution.content.wysiwyg.service.WysiwygController;
import org.silverpeas.core.contribution.contentcontainer.content.ContentManagerException;
import org.silverpeas.core.contribution.model.ContributionIdentifier;
import org.silverpeas.core.contribution.publication.model.PublicationDetail;
import org.silverpeas.core.contribution.publication.model.PublicationPK;
import org.silverpeas.core.contribution.publication.service.PublicationService;
import org.silverpeas.core.i18n.I18NHelper;
import org.silverpeas.core.index.indexing.model.IndexManager;
import org.silverpeas.core.io.media.image.thumbnail.control.ThumbnailController;
import org.silverpeas.core.io.upload.UploadedFile;
import org.silverpeas.core.notification.system.ResourceEvent;
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.model.PdcPosition;
import org.silverpeas.core.pdc.pdc.service.PdcManager;
import org.silverpeas.core.pdc.subscription.service.PdcSubscriptionManager;
import org.silverpeas.core.persistence.Transaction;
import org.silverpeas.core.persistence.jdbc.DBUtil;
import org.silverpeas.core.reminder.Reminder;
import org.silverpeas.core.security.authorization.ComponentAccessControl;
import org.silverpeas.core.silverstatistics.access.service.StatisticService;
import org.silverpeas.kernel.bundle.LocalizationBundle;
import org.silverpeas.kernel.bundle.SettingBundle;
import org.silverpeas.kernel.util.StringUtil;
import org.silverpeas.kernel.logging.SilverLogger;

import javax.inject.Inject;
import javax.inject.Named;
import javax.transaction.Transactional;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.Collections.singletonList;
import static java.util.function.Predicate.not;
import static org.silverpeas.components.quickinfo.notification.QuickInfoDelayedVisibilityUserNotificationReminder.QUICKINFO_DELAYED_VISIBILITY_USER_NOTIFICATION;
import static org.silverpeas.core.contribution.attachment.AttachmentServiceProvider.getAttachmentService;
import static org.silverpeas.core.pdc.pdc.model.PdcClassification.aPdcClassificationOfContent;

@Service
@Named("quickinfoService")
public class DefaultQuickInfoService implements QuickInfoService {

  private static final String ASSOCIATED_TO_THE_NEWS_MSG = " associated to the news ";
  private static final Predicate<News> VISIBLE_PREDICATE = n -> !n.isDraft() && n.isVisible();

  @Inject
  private NewsRepository newsRepository;
  @Inject
  private QuickInfoContentManager quickInfoContentManager;
  @Inject
  private NewsEventNotifier notifier;
  @Inject
  private PdcManager pdcManager;
  @Inject
  private PdcSubscriptionManager pdcSubscriptionManager;

  @Override
  public Optional<News> getContributionById(ContributionIdentifier contributionId) {
    return Optional.of(getNews(contributionId.getLocalId()));
  }

  @Override
  public List<News> getVisibleNews(String componentId) {
    return getAllNews(componentId).stream()
        .filter(VISIBLE_PREDICATE)
        .sorted(QuickInfoDateComparatorDesc.comparator)
        .collect(Collectors.toList());
  }

  @Override
  public List<News> getAllNews(String componentId) {
    final List<News> allNews = newsRepository.getByComponentId(componentId);
    final boolean delegateNewsEnabled = isDelegatedNewsActivated(componentId);
    decorateNews(allNews, delegateNewsEnabled);
    return allNews;
  }

  @Override
  public NewsByStatus getAllNewsByStatus(String componentId, String userId) {
    return new NewsByStatus(getAllNews(componentId), userId);
  }

  @Override
  public News getNews(String id) {
    final News news = newsRepository.getById(id);
    decorateNews(singletonList(news), true);
    return news;
  }

  @Override
  public News getNewsByForeignId(String foreignId) {
    final News news = newsRepository.getByForeignId(foreignId);
    decorateNews(singletonList(news), true);
    return news;
  }

  @Override
  public void acknowledgeNews(String id, String userId) {
    News news = newsRepository.getById(id);
    if (news != null) {
      getStatisticService().addStat(userId, news);
    }
  }

  @Override
  public SettingBundle getComponentSettings() {
    return QuickInfoComponentSettings.getSettings();
  }

  @Override
  public LocalizationBundle getComponentMessages(String language) {
    return QuickInfoComponentSettings.getMessagesIn(language);
  }

  @Override
  public boolean isRelatedTo(final String instanceId) {
    return instanceId.startsWith(QuickInfoComponentSettings.COMPONENT_NAME);
  }

  @Override
  @Transactional
  public News create(final News news) {
    ResourceReference volatileAttachmentSourceRef =
        new ResourceReference(news.getPublicationId(), news.getComponentInstanceId());

    // Creating publication
    final PublicationDetail publication = news.getPublication();
    publication.setIndexOperation(IndexManager.NONE);
    final PublicationPK pubPK = getPublicationService().createPublication(publication);
    publication.setPk(pubPK);

    // Updating the news
    news.setId(null);
    news.setPublicationId(pubPK.getId());
    news.createdBy(publication.getCreatorId());
    final News savedNews = newsRepository.save(news);

    // Attaching all documents linked to volatile news to the persisted news
    List<SimpleDocumentPK> movedDocumentPks = AttachmentServiceProvider.getAttachmentService()
        .moveAllDocuments(volatileAttachmentSourceRef,
            savedNews.getPublication().getPK().toResourceReference());
    if (!movedDocumentPks.isEmpty()) {
      // Change images path in wysiwyg
      WysiwygController.wysiwygPlaceHaveChanged(news.getComponentInstanceId(),
          volatileAttachmentSourceRef.getId(), news.getComponentInstanceId(), savedNews.getId());
    }

    // Referring new content into taxonomy
    try {
      quickInfoContentManager
          .createSilverContent(null, publication, publication.getCreatorId(), false);
    } catch (ContentManagerException e) {
      SilverLogger.getLogger(this).error(
          "can not create a silver-content for publication " + publication.getId() +
              " associated to the saved news " + savedNews.getId(), e);
    }

    return savedNews;
  }

  @Override
  @Transactional
  public News copyNews(final News newsToCopy, final PasteDetail pasteDetail) {

    // Initializing the news instance
    final News news = News.builder(newsToCopy).build();
    news.setDraft();
    news.setComponentInstanceId(pasteDetail.getToComponentId());
    news.setCreatorId(pasteDetail.getUserId());

    // Creating linked publication
    PublicationDetail publication = news.getPublication();
    publication.setIndexOperation(IndexManager.NONE);
    final PublicationPK pubPK = getPublicationService().createPublication(publication);
    publication = getPublicationService().getDetail(pubPK);

    // Saving the new news instance into repository
    news.setPublicationId(pubPK.getId());
    news.setPublication(publication);
    final News savedNews = newsRepository.save(news);

    // Copying decorators
    final ResourceReference pubSourceRef = newsToCopy.getPublication().getPK().toReference();
    final ResourceReference pubDestRef = news.getPublication().getPK().toReference();
    // - PDC
    copyPdcPositions(newsToCopy, savedNews);
    // - attachment files
    getAttachmentService()
        .listDocumentsByForeignKeyAndType(pubSourceRef, DocumentType.attachment, null)
        .forEach(a -> getAttachmentService().copyDocument(a, pubDestRef));
    // - WYSIWYG content
    WysiwygController.copy(pubSourceRef.getComponentInstanceId(), pubSourceRef.getLocalId(),
        pubDestRef.getComponentInstanceId(), pubDestRef.getLocalId(), pasteDetail.getUserId());
    // - thumbnail
    ThumbnailController.copyThumbnail(pubSourceRef, pubDestRef);

    return savedNews;
  }

  private void copyPdcPositions(final News sourceNews, final News destNews) {
    String sourceCmpId = sourceNews.getComponentInstanceId();
    String destCmpId = destNews.getComponentInstanceId();
    int sourceId = quickInfoContentManager.getOrCreateSilverContentId(sourceNews.getPublication());
    int destId = quickInfoContentManager.getOrCreateSilverContentId(destNews.getPublication());
    try {
      pdcManager.copyPositions(sourceId, sourceCmpId, destId, destCmpId);
    } catch (PdcException e) {
      SilverLogger.getLogger(this).error(
          "can not copy pdc positions from publication {0} of news {1} to publication {2} of news {3}",
          new Object[]{sourceNews.getPublication().getPK(), sourceNews.getPK(),
              destNews.getPublication().getPK(), destNews.getPK()}, e);
    }
  }

  @Override
  @Transactional
  public void publish(String id, String userId) {
    News news = getNews(id);
    news.setPublishedBy(userId);
    news.setPublished();
    news.setPublishDate(new Date());
    news.lastUpdatedBy(news.getPublishedBy());
    newsRepository.save(news);
    PublicationDetail publication = news.getPublication();
    getPublicationService().setDetail(publication, false);
    try {
      quickInfoContentManager.updateSilverContentVisibility(publication, true);
    } catch (ContentManagerException e) {
      SilverLogger.getLogger(this)
          .error("can not update the silver-content of the publication " + publication.getId() +
              ASSOCIATED_TO_THE_NEWS_MSG + news.getId(), e);
    }
    sendSubscriptionsNotification(news, NotifAction.CREATE);
  }

  @Override
  @Transactional
  public void update(final News news, List<PdcPosition> positions,
      Collection<UploadedFile> uploadedFiles, final boolean forcePublishing) {
    final News before = Transaction.performInNew(() -> getNews(news.getId()));

    final PublicationDetail publication = news.getPublication();

    // saving WYSIWYG content
    WysiwygController.save(news.getContentToStore(), news.getComponentInstanceId(),
        news.getPublicationId(),
            publication.getUpdaterId(), I18NHelper.DEFAULT_LANGUAGE, false);

    // Attach uploaded files
    Attachments.from(uploadedFiles).attachTo(news.getPublication());

    // Updating the publication
    if (news.isDraft()) {
      publication.setIndexOperation(IndexManager.NONE);
    }
    getPublicationService().setDetail(publication);

    // Updating the news
    news.setPublicationId(publication.getId());
    if (forcePublishing) {
      news.setPublishDate(new Date());
      news.setPublishedBy(news.getLastUpdaterId());
    }
    newsRepository.save(news);

    // Updating visibility onto taxonomy
    try {
      quickInfoContentManager.updateSilverContentVisibility(publication, !news.isDraft());
    } catch (ContentManagerException e) {
      SilverLogger.getLogger(this).error(
          "can not update the silver-content of the publication " + publication.getId() +
              ASSOCIATED_TO_THE_NEWS_MSG + news.getId(), e);
    }

    // Classifying new content onto taxonomy
    classifyQuickInfo(publication, positions);

    notifier.notifyEventOn(ResourceEvent.Type.UPDATE, before, news);

    // Sending notifications to subscribers
    sendSubscriptionsNotification(news, forcePublishing ? NotifAction.CREATE: NotifAction.UPDATE);
  }

  @Override
  @Transactional
  public void removeNews(final String id) {
    final News news = getNews(id);

    final PublicationPK foreignPK = news.getForeignPK();

    // Deleting publication
    getPublicationService().removePublication(foreignPK);

    // De-reffering contribution in taxonomy
    try (final Connection connection = DBUtil.openConnection()) {
      quickInfoContentManager.deleteSilverContent(connection, foreignPK);
    } catch (ContentManagerException | SQLException e) {
      SilverLogger.getLogger(this).error(
          "can not delete the silver-content of the publication " + foreignPK.getId() +
              ASSOCIATED_TO_THE_NEWS_MSG + news.getId(), e);
    }

    // TODO: the statistic deletion should be done by using the CDI notification for a better decoupling

    // deleting statistics
    getStatisticService().deleteStats(news);

    // deleting news itself
    newsRepository.deleteById(id);

    notifier.notifyEventOn(ResourceEvent.Type.DELETION, news);
  }

  @Override
  public List<News> getPlatformNews(String userId) {
    SilverLogger.getLogger(this).debug("Enter Get All Quick Info : User=" + userId);
    final String[] allowedComponentIds = OrganizationController.get()
        .getComponentIdsForUser(userId, QuickInfoComponentSettings.COMPONENT_NAME);
    int limit = QuickInfoComponentSettings.getSettings().getInteger("news.all.limit", 30);
    //noinspection SimplifyStreamApiCallChains
    return Optional.ofNullable(allowedComponentIds)
        .map(Arrays::asList)
        .filter(not(List::isEmpty))
        .map(newsRepository::getByComponentIds)
        .stream()
        .flatMap(List::stream)
        .filter(n -> n.getPublishDate() != null)
        .map(n -> {
          decorateNews(singletonList(n), false);
          return n;
        })
        .filter(VISIBLE_PREDICATE)
        .limit(limit)
        .collect(Collectors.toList());
  }

  @Override
  public List<News> getNewsForTicker(String userId) {
    final List<News> tickerNews = filterAuthorized(newsRepository.getTickerNews(), userId)
        .collect(Collectors.toList());
    if (tickerNews.isEmpty()) {
      return tickerNews;
    }
    decorateNews(tickerNews, false);
    return tickerNews.stream()
        .filter(VISIBLE_PREDICATE)
        .sorted(QuickInfoDateComparatorDesc.comparator)
        .collect(Collectors.toList());
  }

  @Override
  public List<News> getUnreadBlockingNews(String userId) {
    final List<News> blockingNews = filterAuthorized(newsRepository.getBlockingNews(), userId)
        .collect(Collectors.toList());
    if (blockingNews.isEmpty()) {
      return blockingNews;
    }
    decorateNews(blockingNews, false);
    final List<News> visibleNews = blockingNews.stream()
        .filter(VISIBLE_PREDICATE)
        .collect(Collectors.toList());
    final Set<News> readNews = getStatisticService()
        .filterRead(visibleNews, userId)
        .collect(Collectors.toSet());
    return visibleNews.stream()
        .filter(n -> !readNews.contains(n))
        .sorted(QuickInfoDateComparatorDesc.comparator)
        .collect(Collectors.toList());
  }

  private Stream<News> filterAuthorized(final List<News> news, final String userId) {
    final Set<String> allInstanceIds = news.stream()
        .map(News::getComponentInstanceId)
        .collect(Collectors.toSet());
    final Set<String> authorizedInstanceIds = ComponentAccessControl.get()
        .filterAuthorizedByUser(allInstanceIds, userId)
        .collect(Collectors.toSet());
    return news.stream()
        .filter(n -> authorizedInstanceIds.contains(n.getComponentInstanceId()));
  }

  @Override
  public void submitNewsOnHomepage(String id, String userId) {
    News news = getNews(id);
    news.setId(news.getPublicationId());
    getDelegatedNewsService().submitNews(news,
        news.getVisibility().getSpecificPeriod().orElse(null), userId);
  }

  @Override
  public void performReminder(final Reminder reminder) {
    if (QUICKINFO_DELAYED_VISIBILITY_USER_NOTIFICATION.asString().equals(reminder.getProcessName())) {
      getContributionById(reminder.getContributionId())
          .ifPresent(n -> sendSubscriptionsNotification(n, NotifAction.CREATE));
    }
  }

  private void sendSubscriptionsNotification(final News news, final NotifAction notifAction) {
    if (!news.isDraft()) {
      if (news.isVisible()) {
        new QuickInfoSubscriptionUserNotification(news, notifAction).build().send();
        // send notification if PDC subscription
        try {
          final PublicationPK pubPK = news.getPublication().getPK();
          int silverObjectId = quickInfoContentManager.getSilverContentId(pubPK.getId(), pubPK.getInstanceId());
          List<ClassifyPosition> positions = pdcManager.getPositions(silverObjectId, pubPK
              .getInstanceId());
          if (positions != null) {
            for (ClassifyPosition position : positions) {
              pdcSubscriptionManager.checkSubscriptions(position.getValues(), pubPK
                  .getInstanceId(), silverObjectId);
            }
          }
        } catch (PdcException e) {
          SilverLogger.getLogger(this)
              .error("PdC subscriber notification failure", e);
        }
      } else {
        QuickInfoDelayedVisibilityUserNotificationReminder.get().setAbout(news);
      }
    }
  }

  /**
   * Classify the info letter publication on the PdC only if the positions parameter is filled
   * @param publi the quickInfo PublicationDetail to classify
   * @param pdcPositions the string json positions
   */
  private void classifyQuickInfo(PublicationDetail publi, List<PdcPosition> pdcPositions) {
    if (pdcPositions != null) {
      PdcClassification classification =
          aPdcClassificationOfContent(publi).withPositions(pdcPositions);
      classification.classifyContentOrClearClassificationIfEmpty(publi, false);
    }
  }

  private boolean isDelegatedNewsActivated(String componentId) {
    String paramValue = OrganizationControllerProvider.getOrganisationController()
        .getComponentParameterValue(componentId, QuickInfoComponentSettings.PARAM_DELEGATED);
    return StringUtil.getBooleanValue(paramValue);
  }

  private void decorateNews(final List<News> news, final boolean delegated) {
    final Map<String, News> mapping = mapByPublicationId(news);
    getPublicationService().getByIds(mapping.keySet()).forEach(p -> {
      News current = mapping.get(p.getId());
      current.setPublication(p);
    });
    if (delegated) {
      getDelegatedNewsService().getDelegatedNews(mapping.keySet()).forEach(d -> {
        News current = mapping.get(d.getId());
        current.setDelegatedNews(d);
      });
    }
  }

  private Map<String, News> mapByPublicationId(final List<News> news) {
    final Map<String, News> mapping = new HashMap<>(news.size());
    news.stream().filter(Objects::nonNull).forEach(n -> mapping.put(n.getPublicationId(), n));
    return mapping;
  }


  private PublicationService getPublicationService() {
    return PublicationService.get();
  }

  private StatisticService getStatisticService() {
    return StatisticService.get();
  }

  private DelegatedNewsService getDelegatedNewsService() {
    return DelegatedNewsServiceProvider.getDelegatedNewsService();
  }

}