# KmeliaBinsScheduledPurger.java

    /*
     * Copyright (C) 2000 - 2025 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.silverpeas.core.admin.component.model.WAComponent;
    import org.silverpeas.core.admin.service.AdminException;
    import org.silverpeas.core.admin.service.ComponentInstManager;
    import org.silverpeas.core.annotation.Service;
    import org.silverpeas.core.contribution.publication.model.PublicationDetail;
    import org.silverpeas.core.contribution.publication.service.PublicationService;
    import org.silverpeas.core.initialization.Initialization;
    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.persistence.Transaction;
    import org.silverpeas.core.scheduler.*;
    import org.silverpeas.core.scheduler.trigger.JobTrigger;
    import org.silverpeas.kernel.SilverpeasRuntimeException;
    import org.silverpeas.kernel.annotation.NonNull;
    import org.silverpeas.kernel.bundle.ResourceLocator;
    import org.silverpeas.kernel.bundle.SettingBundle;
    import org.silverpeas.kernel.logging.SilverLogger;

    import jakarta.inject.Inject;
    import java.text.ParseException;
    import java.time.LocalDate;
    import java.util.Date;
    import java.util.stream.Stream;

    import static org.silverpeas.core.util.DateUtil.toLocalDate;

    /**
     * A service scheduled in time to clean out periodically the bin in each Kmelia instances of the
     * items (both topics and publications) that have been removed since a given number of days. The
     * purge scheduling and the delay in days can be parameterized in the
     * <code>org/silverpeas/kmelia/settings/kmeliaSettings.properties</code> configuration file.
     *
     * @author mmoquillon
     */
    @Service
    public class KmeliaBinsScheduledPurger implements Initialization {

      private static final String COMPONENT_NAME = "kmelia";
      private static final String SETTINGS_NAME = "org.silverpeas.kmelia.settings.kmeliaSettings";
      private static final String JOB_NAME = "BinOlderItemsDeleter";

      @Inject
      private KmeliaDeleter deleter;
      @Inject
      private ComponentInstManager componentInstManager;
      @Inject
      private NodeService nodeService;
      @Inject
      private PublicationService publicationService;

      @Override
      public void init() throws SchedulerException {
        String cron = getSchedulingCron();
        if (!cron.isEmpty()) {
          final Scheduler scheduler = SchedulerProvider.getVolatileScheduler();
          scheduler.unscheduleJob(JOB_NAME);
          scheduler.scheduleJob(new BinOlderItemsDeleter(), eachTimeAt(cron));
        }
        new BinOlderItemsDeleter().execute(JobExecutionContext.createWith(JOB_NAME, new Date()));
      }

      @NonNull
      private static JobTrigger eachTimeAt(String cron) throws SchedulerException {
        try {
          return JobTrigger.triggerAt(cron);
        } catch (ParseException e) {
          throw new SchedulerException(e.getMessage(), e);
        }
      }

      @Override
      public void release() throws SchedulerException {
        Scheduler scheduler = SchedulerProvider.getVolatileScheduler();
        if (scheduler.isJobScheduled(JOB_NAME)) {
          scheduler.unscheduleJob(JOB_NAME);
        }
      }

      private String getSchedulingCron() {
        SettingBundle settings = ResourceLocator.getSettingBundle(SETTINGS_NAME);
        return settings.getString("kmelia.autoDeletionCron", "");
      }

      private class BinOlderItemsDeleter extends Job {

        BinOlderItemsDeleter() {
          super(JOB_NAME);
        }

        @Override
        public void execute(final JobExecutionContext context) {
          int delay = getDeletionDelay();
          if (delay > 0) {
            SilverLogger.getLogger(this)
                .info("Purge Kmelia bins from contributions removed more than " + delay + " days");
            try {
              // browse all the root items in each Kmelia bins and for each of them apply the purge algo
              Transaction.performInOne(() -> {
                final LocalDate now = LocalDate.now();
                WAComponent.getByName(COMPONENT_NAME)
                    .ifPresent(component ->
                        getAllInstanceIds(component)
                            .map(id -> COMPONENT_NAME + id)
                            .forEach(instanceId -> {
                              NodePK bin = new NodePK(NodePK.BIN_NODE_ID, instanceId);
                              nodeService.getChildrenDetails(bin).stream()
                                  .filter(node -> isOlder(node, now))
                                  .forEach(topic ->
                                      deleter.deleteTopic(topic.getNodePK()));
                              publicationService.getDetailsByFatherPK(bin).stream()
                                  .filter(publication -> isOlder(publication, now))
                                  .forEach(publication ->
                                      deleter.deletePublication(publication.getPK()));
                            }));
                return null;
              });
            } catch (SilverpeasRuntimeException e) {
              SilverLogger.getLogger(this).error(e.getMessage(), e);
            }
          }
        }

        private int getDeletionDelay() {
          SettingBundle settings = ResourceLocator.getSettingBundle(SETTINGS_NAME);
          int delay = settings.getInteger("kmelia.autoDeletionDelay", 0);
          return Math.max(delay, 0);
        }

        private boolean isOlder(PublicationDetail publication, LocalDate date) {
          // old publication in bin before the adding of the removal properties. In such situation, it
          // is automatically deleted
          if (!publication.isRemoved()) {
            return true;
          }
          return isOlder(publication.getRemovalDate(), date);
        }

        private boolean isOlder(NodeDetail topic, LocalDate date) {
          // old topic in bin before the adding of the removal properties. In such situation, it is
          // automatically deleted
          if (!topic.isRemoved()) {
            return true;
          }
          return isOlder(topic.getRemovalDate(), date);
        }

        private boolean isOlder(Date removalDate, LocalDate date) {
          final LocalDate removeDayDateWithDelay = toLocalDate(removalDate)
              .plusDays(getDeletionDelay());
          return removeDayDateWithDelay.isBefore(date) ||
              removeDayDateWithDelay.isEqual(date);
        }

        private Stream<String> getAllInstanceIds(WAComponent component) {
          try {
            return Stream.of(componentInstManager.getAllCompoIdsByComponentName(component.getName()));
          } catch (AdminException e) {
            throw new SilverpeasRuntimeException(e);
          }
        }
      }
    }