InfoLetterDataManager.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.infoletter.implementation;

import org.silverpeas.components.infoletter.InfoLetterContentManager;
import org.silverpeas.components.infoletter.InfoLetterException;
import org.silverpeas.components.infoletter.model.*;
import org.silverpeas.core.ApplicationServiceProvider;
import org.silverpeas.core.ResourceReference;
import org.silverpeas.core.WAPrimaryKey;
import org.silverpeas.core.admin.component.model.ComponentInst;
import org.silverpeas.core.admin.service.OrganizationController;
import org.silverpeas.core.admin.service.OrganizationControllerProvider;
import org.silverpeas.core.admin.user.model.Group;
import org.silverpeas.core.admin.user.model.User;
import org.silverpeas.core.admin.user.model.UserDetail;
import org.silverpeas.core.annotation.Service;
import org.silverpeas.core.contribution.attachment.AttachmentServiceProvider;
import org.silverpeas.core.contribution.attachment.model.DocumentType;
import org.silverpeas.core.contribution.attachment.model.SimpleDocument;
import org.silverpeas.core.contribution.content.wysiwyg.service.WysiwygContentTransformer;
import org.silverpeas.core.contribution.content.wysiwyg.service.WysiwygController;
import org.silverpeas.core.contribution.model.Contribution;
import org.silverpeas.core.contribution.model.ContributionIdentifier;
import org.silverpeas.core.i18n.I18NHelper;
import org.silverpeas.core.index.indexing.model.FullIndexEntry;
import org.silverpeas.core.index.indexing.model.IndexEngineProxy;
import org.silverpeas.core.index.indexing.model.IndexEntryKey;
import org.silverpeas.core.mail.MailAddress;
import org.silverpeas.core.persistence.jdbc.DBUtil;
import org.silverpeas.core.persistence.jdbc.bean.*;
import org.silverpeas.core.persistence.jdbc.sql.JdbcSqlQuery;
import org.silverpeas.core.subscription.Subscription;
import org.silverpeas.core.subscription.SubscriptionServiceProvider;
import org.silverpeas.core.subscription.service.*;
import org.silverpeas.core.subscription.util.SubscriptionSubscriberList;
import org.silverpeas.core.util.DateUtil;
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 javax.inject.Inject;
import javax.inject.Named;
import javax.mail.internet.InternetAddress;
import javax.transaction.Transactional;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.*;

import static java.util.stream.Collectors.toSet;
import static org.silverpeas.core.mail.MailAddress.eMail;
import static org.silverpeas.core.mail.ReceiverMailAddressSet.with;

@SuppressWarnings({"deprecation", "SqlNoDataSourceInspection"})
@Service
@Named("infoLetter" + ApplicationServiceProvider.SERVICE_NAME_SUFFIX)
public class InfoLetterDataManager implements InfoLetterService {

  private static final String MESSAGES_PATH
      = "org.silverpeas.infoLetter.multilang.infoLetterBundle";
  private static final String SETTINGS_PATH
      = "org.silverpeas.infoLetter.settings.infoLetterSettings";
  private static final SettingBundle settings = ResourceLocator.getSettingBundle(SETTINGS_PATH);

  private static final String TABLE_EXTERNAL_EMAILS = "SC_IL_ExtSus";
  private static final String INSTANCE_ID = "instanceId";

  private final SilverpeasBeanDAO<InfoLetter> infoLetterDAO;
  private final SilverpeasBeanDAO<InfoLetterPublication> infoLetterPublicationDAO;

  @Inject
  private InfoLetterContentManager infoLetterContentManager;

  @SuppressWarnings("unchecked")
  @Override
  public <T extends Contribution> Optional<T> getContributionById(
      final ContributionIdentifier contributionId) {
    if (InfoLetterPublicationPdC.TYPE.equals(contributionId.getType())) {
      final String localId = contributionId.getLocalId();
      final IdPK pk = new IdPK(localId);
      return (Optional<T>) Optional.ofNullable(getInfoLetterPublication(pk));
    } else if (InfoLetter.TYPE.equals(contributionId.getType())) {
      return (Optional<T>) getInfoLetters(contributionId.getComponentInstanceId()).stream()
          .map(InfoLetterTemplateContributionWrapper::new)
          .findFirst();
    }
    throw new IllegalStateException(
        MessageFormat.format("type {0} is not handled", contributionId.getType()));
  }

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

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

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

  public InfoLetterDataManager() {
    try {
      infoLetterDAO = SilverpeasBeanDAOFactory.getDAO(InfoLetter.class);
      infoLetterPublicationDAO = SilverpeasBeanDAOFactory.getDAO(InfoLetterPublication.class);
    } catch (PersistenceException pe) {
      throw new InfoLetterException(pe);
    }
  }

  /**
   * Implementation of InfoLetterService interface
   */
  @Override
  public void createInfoLetter(InfoLetter il) {
    try {
      WAPrimaryKey pk = infoLetterDAO.add(il);
      il.setPK(pk);
    } catch (PersistenceException pe) {
      throw new InfoLetterException(pe);
    }
  }

  @Override
  public void updateInfoLetter(InfoLetter ie) {
    try {
      infoLetterDAO.update(ie);
      deleteIndex(ie);
      createIndex(ie);
    } catch (PersistenceException pe) {
      throw new InfoLetterException(pe);
    }
  }

  @Override
  public List<InfoLetter> getInfoLetters(String instanceId) {
    BeanCriteria criteria = BeanCriteria.addCriterion(INSTANCE_ID, instanceId);
    try {
      return new ArrayList<>(infoLetterDAO.findBy(criteria));
    } catch (PersistenceException pe) {
      throw new InfoLetterException(pe);
    }
  }

  @Override
  public List<InfoLetterPublication> getInfoLetterPublications(WAPrimaryKey letterPK) {
    try {
      InfoLetter letter = getInfoLetter(letterPK);
      BeanCriteria criteria = BeanCriteria.addCriterion(INSTANCE_ID, letter.getInstanceId())
          .and("letterId", Integer.parseInt(letterPK.getId()));
      criteria.setDescOrderBy("id");
      return new ArrayList<>(infoLetterPublicationDAO.findBy(criteria));
    } catch (PersistenceException pe) {
      throw new InfoLetterException(pe);
    }
  }

  @Override
  public void createInfoLetterPublication(InfoLetterPublicationPdC ilp, String userId) {

    Connection con = openConnection();

    try {
      WAPrimaryKey pk = infoLetterPublicationDAO.add(con, ilp);
      ilp.setPK(pk);
      infoLetterContentManager.createSilverContent(con, ilp, userId);
    } catch (Exception pe) {
      DBUtil.rollback(con);
      throw new InfoLetterException(pe);
    } finally {
      DBUtil.close(con);
    }
  }

  @Transactional
  @Override
  public void deleteInfoLetterPublication(WAPrimaryKey pk, String componentId) {
    try (Connection con = openConnection()) {
      infoLetterPublicationDAO.remove(pk);
      infoLetterContentManager.deleteSilverContent(con, pk.getId(), componentId);
      final InfoLetterPublication entity = new InfoLetterPublication(
          new ResourceReference(pk.getId(), componentId), componentId, null, null, null, 0, 0);
      deleteIndex(entity);
      entity.deleteContent();
    } catch (Exception pe) {
      throw new InfoLetterException(pe);
    }
  }

  @Override
  public void updateInfoLetterPublication(InfoLetterPublicationPdC ilp) {
    try {
      infoLetterPublicationDAO.update(ilp);
      infoLetterContentManager.updateSilverContentVisibility(ilp);
      if (ilp.isValid()) {
        deleteIndex(ilp);
        createIndex(ilp);
      }
    } catch (Exception e) {
      throw new InfoLetterException(e);
    }
  }

  @Override
  public InfoLetter getInfoLetter(WAPrimaryKey letterPK) {
    InfoLetter infoLetter;
    try {
      infoLetter = infoLetterDAO.findByPrimaryKey(letterPK);
    } catch (PersistenceException pe) {
      throw new InfoLetterException(pe);
    }
    return infoLetter;
  }

  @Override
  public InfoLetterPublicationPdC getInfoLetterPublication(WAPrimaryKey publiPK) {
    InfoLetterPublicationPdC classifiedPublic;
    try {
      classifiedPublic = new InfoLetterPublicationPdC(infoLetterPublicationDAO.findByPrimaryKey(publiPK));
    } catch (PersistenceException pe) {
      throw new InfoLetterException(pe);
    }
    return classifiedPublic;
  }

  @Override
  public InfoLetter createDefaultLetter(String componentId) {
    OrganizationController oc = OrganizationControllerProvider.getOrganisationController();
    ComponentInst ci = oc.getComponentInst(componentId);
    InfoLetter ie = new InfoLetter();
    ie.setInstanceId(componentId);
    ie.setName(ci.getLabel());
    createInfoLetter(ie);
    initTemplate(componentId, ie.getPK(), "0");
    return ie;
  }

  /**
   * Deletes all the info letters (and then all the publications and external subscribers) in the
   * specified component instance.
   * @param componentId the unique identifier of the InfoLetter instance.
   */
  @Override
  public void deleteAllInfoLetters(final String componentId) {
    try (Connection connection = openConnection()) {
      BeanCriteria criteria = BeanCriteria.addCriterion(INSTANCE_ID, componentId);
      infoLetterPublicationDAO.removeBy(connection, criteria);
      infoLetterDAO.removeBy(connection, criteria); //TABLE_EXTERNAL_EMAILS
      try (PreparedStatement statement = connection.prepareStatement(
          "delete from " + TABLE_EXTERNAL_EMAILS + " where instanceId = ?")) {
        statement.setString(1, componentId);
        statement.execute();
      }
    } catch (Exception e) {
      throw new InfoLetterException(e);
    }
  }

  @Override
  public int getSilverObjectId(String pubId, String componentId) {

    try {
      int silverObjectId = infoLetterContentManager.getSilverContentId(pubId, componentId);
      if (silverObjectId == -1) {
        IdPK publiPK = new IdPK();
        publiPK.setId(pubId);
        InfoLetterPublicationPdC infoLetter = getInfoLetterPublication(publiPK);
        silverObjectId = infoLetterContentManager
            .createSilverContent(null, infoLetter, infoLetter.getCreatorId());
      }
      return silverObjectId;
    } catch (Exception e) {
      throw new InfoLetterException(e);
    }
  }

  @Override
  public SubscriptionSubscriberList getInternalSuscribers(final String componentId) {
    return ResourceSubscriptionProvider.getSubscribersOfComponent(componentId);
  }

  @Override
  public void setInternalSuscribers(final String componentId, final UserDetail[] users,
      final Group[] groups) {

    // Initializing necessary subscriptions
    Collection<Subscription> subscriptions = new ArrayList<>(users.length + groups.length);
    for (UserDetail user : users) {
      subscriptions.add(
          new ComponentSubscription(UserSubscriptionSubscriber.from(user.getId()), componentId));
    }
    for (Group group : groups) {
      subscriptions.add(
          new ComponentSubscription(GroupSubscriptionSubscriber.from(group.getId()), componentId));
    }

    // Getting all existing subscriptions and selecting those that have to be deleted
    Collection<Subscription> subscriptionsToDelete =
        SubscriptionServiceProvider.getSubscribeService()
            .getByResource(ComponentSubscriptionResource.from(componentId));
    subscriptionsToDelete.removeAll(subscriptions);

    // Deleting
    SubscriptionServiceProvider.getSubscribeService().unsubscribe(subscriptionsToDelete);

    // Creating subscriptions (nothing is registered for subscriptions that already exist)
    SubscriptionServiceProvider.getSubscribeService().subscribe(subscriptions);
  }

  @Override
  public Set<String> getEmailsExternalsSuscribers(WAPrimaryKey letterPK) {
    Set<String> emails = new LinkedHashSet<>();
    try (Connection con = openConnection()) {
      InfoLetter letter = getInfoLetter(letterPK);
      String selectQuery =
          "SELECT * FROM " + TABLE_EXTERNAL_EMAILS + " where instanceId = ? and letter = ?";
      try (PreparedStatement selectStmt = con.prepareStatement(selectQuery)) {
        selectStmt.setString(1, letter.getInstanceId());
        selectStmt.setInt(2, Integer.parseInt(letterPK.getId()));
        try (ResultSet rs = selectStmt.executeQuery()) {
          while (rs.next()) {
            emails.add(rs.getString("email"));
          }
        }
      }
    } catch (Exception e) {
      throw new InfoLetterException(e);
    }
    return emails;
  }

  @Transactional
  @Override
  public void setEmailsExternalsSubscribers(WAPrimaryKey letterPK, Set<String> emails) {
    try (Connection con = openConnection()) {
      final InfoLetter letter = getInfoLetter(letterPK);
      final int letterId = Integer.parseInt(letterPK.getId());
      JdbcSqlQuery.deleteFrom(TABLE_EXTERNAL_EMAILS)
          .where("instanceId = ?", letter.getInstanceId())
          .and("letter = ?", letterId)
          .executeWith(con);
      for (String email : emails) {
        JdbcSqlQuery.insertInto(TABLE_EXTERNAL_EMAILS)
            .withInsertParam("letter", letterId)
            .withInsertParam("email", email)
            .withInsertParam(INSTANCE_ID, letter.getInstanceId())
            .executeWith(con);
      }
    } catch (Exception e) {
      throw new InfoLetterException(e);
    }
  }

  @Override
  public void toggleSuscriber(String userId, String componentId, boolean isUserSubscribing) {
    Subscription subscription =
        new ComponentSubscription(UserSubscriptionSubscriber.from(userId), componentId);
    if (isUserSubscribing) {
      SubscriptionServiceProvider.getSubscribeService().subscribe(subscription);
    } else {
      SubscriptionServiceProvider.getSubscribeService().unsubscribe(subscription);
    }
  }

  @Override
  public boolean isUserSuscribed(String userId, String componentId) {
    return SubscriptionServiceProvider.getSubscribeService().existsSubscription(
        new ComponentSubscription(UserSubscriptionSubscriber.from(userId), componentId));
  }

  @Override
  public void initTemplate(String componentId, WAPrimaryKey letterPK, String userId) {
    try {
      String basicTemplate = "<body></body>";
      WysiwygController.createUnindexedFileAndAttachment(basicTemplate,
          new ResourceReference(InfoLetter.TEMPLATE_ID + letterPK.getId(), componentId), userId,
          I18NHelper.DEFAULT_LANGUAGE);
    } catch (Exception e) {
      throw new InfoLetterException(e);
    }
  }

  private Connection openConnection() {
    Connection con;
    try {
      con = DBUtil.openConnection();
    } catch (Exception e) {
      throw new InfoLetterException(e);
    }
    return con;
  }

  @Override
  public Set<String> sendTemplateByMail(final InfoLetter il, final String mimeMultipart,
      final Set<String> listEmailDest, final String subject, final String emailFrom) {
    return sendContributionByMail(il.getTemplateIdentifier(), mimeMultipart, listEmailDest, subject,
        emailFrom);
  }

  @Override
  public Set<String> sendLetterByMail(InfoLetterPublicationPdC ilp, String mimeMultipart,
      Set<String> listEmailDest, String subject, String emailFrom) {
    return sendContributionByMail(ilp.getIdentifier(), mimeMultipart, listEmailDest, subject,
        emailFrom);
  }

  private Set<String> sendContributionByMail(final ContributionIdentifier cId,
      final String mimeMultipart, final Set<String> listEmailDest, final String subject,
      final String emailFrom) {
    final Set<String> emailErrors = new HashSet<>();
    final Set<String> emailDest = new HashSet<>();
    // Verifying emails
    listEmailDest.forEach(m -> {
      try {
        new InternetAddress(m);
        emailDest.add(m);
      } catch (Exception ex) {
        SilverLogger.getLogger(this).error(ex);
        emailErrors.add(m);
      }
    });
    if (!emailDest.isEmpty()) {
      try {
        final ResourceReference foreignKey = cId.toReference();
        final List<SimpleDocument> listAttachedFiles =
            AttachmentServiceProvider.getAttachmentService().
                listDocumentsByForeignKeyAndType(foreignKey, DocumentType.attachment, null);
        final String wysiwygContent =
            WysiwygController.load(foreignKey.getInstanceId(), foreignKey.getId(), null);
        WysiwygContentTransformer.on(wysiwygContent)
            .toMailContent()
            .withMimeMultipart(mimeMultipart)
            .addAttachments(listAttachedFiles)
            .prepareMailSendingFrom(eMail(emailFrom))
            .to(with(listEmailDest.stream().map(MailAddress::eMail).collect(toSet())))
            .withSubject(subject)
            .oneMailPerReceiver()
            .send();
      } catch (Exception e) {
        throw new InfoLetterException(e);
      }
    }
    return emailErrors;
  }

  @Override
  public void indexInfoLetter(final String componentId) {
    final InfoLetter infoLetter = getInfoLetters(componentId).get(0);
    createIndex(infoLetter);
    indexPublications(infoLetter);
  }

  private void indexPublications(final InfoLetter infoLetter) {
    try {
      Optional.ofNullable(getInfoLetterPublications(infoLetter.getPK())).stream()
          .flatMap(Collection::stream)
          .forEach(this::processPublicationIndexation);
    } catch (Exception e) {
      throw new InfoLetterException(e);
    }
  }

  private void processPublicationIndexation(final InfoLetterPublication pub) {
    try {
      if (pub.isValid()) {
        createIndex(pub);
      }
    } catch (Exception e) {
      SilverLogger.getLogger(this)
          .error("Error during indexation of newsletter {0}", pub.getPK().getId(), e);
    }
  }

  private void createIndex(InfoLetter il) {
    if (il != null) {
      final FullIndexEntry indexEntry = new FullIndexEntry(new IndexEntryKey(
          il.getInstanceId(),
          InfoLetter.TYPE,
          il.getPK().getId()));
      indexEntry.setTitle(il.getName());
      indexEntry.setPreview(il.getDescription());
      IndexEngineProxy.addIndexEntry(indexEntry);
    }
  }

  private void deleteIndex(InfoLetter il) {
    final IndexEntryKey indexEntry = new IndexEntryKey(il.getInstanceId(), InfoLetter.TYPE,
        il.getPK().getId());
    IndexEngineProxy.removeIndexEntry(indexEntry);
  }

  private void createIndex(InfoLetterPublication pub) {
    if (pub != null) {
      final ContributionIdentifier identifier = pub.getIdentifier();
      final FullIndexEntry indexEntry = new FullIndexEntry(new IndexEntryKey(
          identifier.getComponentInstanceId(),
          InfoLetterPublicationPdC.TYPE,
          identifier.getLocalId()));
      indexEntry.setTitle(pub.getTitle());
      indexEntry.setPreview(pub.getDescription());
      try {
        indexEntry.setCreationDate(DateUtil.parse(pub.getParutionDate()));
      } catch (ParseException e) {
        SilverLogger.getLogger(this).warn(e);
      }
      indexEntry.setCreationUser(User.getCurrentUser().getId());
      IndexEngineProxy.addIndexEntry(indexEntry);
    }
  }

  private void deleteIndex(InfoLetterPublication pub) {
    final ContributionIdentifier identifier = pub.getIdentifier();
    final IndexEntryKey indexEntry = new IndexEntryKey(identifier.getComponentInstanceId(),
        InfoLetterPublicationPdC.TYPE, identifier.getLocalId());
    IndexEngineProxy.removeIndexEntry(indexEntry);
  }
}