AlbumMediaNotificationManager.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.gallery.notification.user;

import org.silverpeas.components.gallery.model.AlbumDetail;
import org.silverpeas.components.gallery.model.AlbumMedia;
import org.silverpeas.components.gallery.model.Media;
import org.silverpeas.core.admin.user.model.User;
import org.silverpeas.core.annotation.Service;
import org.silverpeas.core.notification.user.builder.helper.UserNotificationHelper;
import org.silverpeas.core.scheduler.Job;
import org.silverpeas.core.scheduler.JobExecutionContext;
import org.silverpeas.core.scheduler.Scheduler;
import org.silverpeas.core.scheduler.SchedulerException;
import org.silverpeas.core.scheduler.SchedulerProvider;
import org.silverpeas.core.scheduler.trigger.JobTrigger;
import org.silverpeas.kernel.bundle.ResourceLocator;
import org.silverpeas.core.util.ServiceProvider;
import org.silverpeas.kernel.bundle.SettingBundle;
import org.silverpeas.kernel.logging.SilverLogger;

import java.time.OffsetDateTime;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * @author silveryocha
 */
@Service
public class AlbumMediaNotificationManager {

  private static final ConcurrentMap<String, NotificationAlbumJob> contextCache =
      new ConcurrentHashMap<>();

  AlbumMediaNotificationManager() {
  }

  private static String buildKey(final User user, final AlbumMedia albumMedia) {
    return buildKey(user, albumMedia.getMedia().getInstanceId(), albumMedia.getAlbumId());
  }

  private static String buildKey(final User user, final String instanceId, String albumId) {
    return user.getId() + "@" + instanceId + "@" + albumId;
  }

  public static AlbumMediaNotificationManager get() {
    return ServiceProvider.getService(AlbumMediaNotificationManager.class);
  }

  /**
   * Puts a creation of an {@link AlbumMedia} into the context.
   * @param albumMedia the couple of an album and a media.
   * @param by the user associated to {@link AlbumMedia}.
   */
  public void putCreationOf(final AlbumMedia albumMedia, final User by) {
    final NotificationAlbumJob job = getNotificationAlbumJob(by, albumMedia);
    job.registerCreation(albumMedia.getMedia());
  }

  /**
   * Puts a deletion of an {@link AlbumMedia} into the context.
   * @param albumMedia the couple of an album and a media.
   * @param by the user associated to {@link AlbumMedia}.
   */
  public void putDeletionOf(final AlbumMedia albumMedia, final User by) {
    final NotificationAlbumJob job = getNotificationAlbumJob(by, albumMedia);
    job.registerDeletion(albumMedia.getMedia());
  }

  private NotificationAlbumJob getNotificationAlbumJob(final User user,
      final AlbumMedia albumMedia) {
    final String key = buildKey(user, albumMedia);
    return contextCache
        .computeIfAbsent(key, k -> new NotificationAlbumJob(user, albumMedia.getAlbum()));
  }

  /**
   * Context of {@link Media} movements according to an album.
   */
  static class NotificationAlbumJob extends Job {
    final Set<Media> creations = Collections.synchronizedSet(new HashSet<>());
    final User sender;
    final String instanceId;
    final AlbumDetail album;
    private final Object mutex;

    private NotificationAlbumJob(final User sender, final AlbumDetail album) {
      super("JOB_NAME_" + UUID.randomUUID().toString());
      this.mutex = creations;
      this.sender = sender;
      this.instanceId = album.getNodePK().getInstanceId();
      this.album = album;
    }

    private void setJobDelay() {
      final SettingBundle settings = ResourceLocator
          .getSettingBundle("org.silverpeas.gallery.settings.gallerySettings");
      final int delayInSeconds = settings.getInteger("subscription.notification.delay", 180);
      boolean sendImmediately = delayInSeconds <= 0;
      if (!sendImmediately) {
        final Scheduler scheduler = SchedulerProvider.getVolatileScheduler();
        final OffsetDateTime executionTime = OffsetDateTime.now().plusSeconds(delayInSeconds);
        try {
          scheduler.unscheduleJob(getName());
          if (isMediaToProcess()) {
            scheduler.scheduleJob(this, JobTrigger.triggerAt(executionTime));
          } else {
            clearJobCache();
          }
        } catch (SchedulerException e) {
          SilverLogger.getLogger(this).error(e);
          sendImmediately = true;
        }
      }
      if (sendImmediately) {
        execute(null);
      }
    }

    void registerCreation(final Media media) {
      synchronized (mutex) {
        creations.add(media);
        setJobDelay();
      }
      putIntoCache();
    }

    void registerDeletion(final Media media) {
      synchronized (mutex) {
        creations.remove(media);
        setJobDelay();
      }
      putIntoCache();
    }

    @Override
    public void execute(final JobExecutionContext context) {
      clearJobCache();
      final Media[] creationsToProcess;
      synchronized (mutex) {
        creationsToProcess = creations.toArray(new Media[0]);
        creations.clear();
      }
      if (creationsToProcess.length > 0) {
        final GalleryAlbumMediaSubscriptionNotificationBuilder builder =
            new GalleryAlbumMediaSubscriptionNotificationBuilder(album, sender)
            .aboutMedia(creationsToProcess);
        UserNotificationHelper.buildAndSend(builder);
      }
    }

    private void clearJobCache() {
      final String cacheKey = buildKey(sender, instanceId, album.getId());
      contextCache.remove(cacheKey);
    }

    private boolean isMediaToProcess() {
      return !creations.isEmpty();
    }

    private void putIntoCache() {
      if (isMediaToProcess()) {
        contextCache.put(buildKey(sender, instanceId, album.getId()),
            NotificationAlbumJob.this);
      }
    }
  }
}