# DefaultQuickInfoService.java

    /*
     * Copyright (C) 2000 - 2026 Silverpeas
     *
     * This program is free software: you can redistribute it and/or modify
     * it under the terms of the GNU Affero General Public License as
     * published by the Free Software Foundation, either version 3 of the
     * License, or (at your option) any later version.
     *
     * As a special exception to the terms and conditions of version 3.0 of
     * the GPL, you may redistribute this Program in connection with Free/Libre
     * Open Source Software ("FLOSS") applications as described in Silverpeas's
     * FLOSS exception. You should have received a copy of the text describing
     * the FLOSS exception, and it is also available here:
     * "https://www.silverpeas.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 jakarta.inject.Inject;
    import jakarta.inject.Named;
    import jakarta.transaction.Transactional;
    import org.silverpeas.components.delegatednews.service.DelegatedNewsService;
    import org.silverpeas.components.quickinfo.NewsByStatus;
    import org.silverpeas.components.quickinfo.NewsSort;
    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.I18n;
    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.logging.SilverLogger;
    import org.silverpeas.kernel.util.StringUtil;

    import java.sql.Connection;
    import java.sql.SQLException;
    import java.util.*;
    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;
      @Inject
      private PublicationService publicationService;
      @Inject
      private StatisticService statisticService;
      @Inject
      private DelegatedNewsService delegatedNewsService;
      @Inject
      private QuickInfoDelayedVisibilityUserNotificationReminder reminder;
      @Inject
      private I18n i18n;

      @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) {

        NewsSort noMoreVisiblesSort = NewsSort.fromString(getComponentSettings().getString("news.visible.noMore.sort"));
        NewsSort notYetVisiblesSort = NewsSort.fromString(getComponentSettings().getString("news.visible.noYet.sort"));

        return new NewsByStatus(getAllNews(componentId), userId, noMoreVisiblesSort, notYetVisiblesSort);
      }

      @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(), i18n.getDefaultLanguage(), 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().deletePublication(foreignPK);

        // De-referring 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);
        }

        // 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 {
            reminder.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;
      }

      private StatisticService getStatisticService() {
        return statisticService;
      }

      private DelegatedNewsService getDelegatedNewsService() {
        return delegatedNewsService;
      }

    }