# KmeliaPublication.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:
     *  "http://www.silverpeas.org/docs/core/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.model;

    import org.silverpeas.components.kmelia.service.KmeliaHelper;
    import org.silverpeas.components.kmelia.service.KmeliaService;
    import org.silverpeas.core.ResourceReference;
    import org.silverpeas.core.SilverpeasExceptionMessages;
    import org.silverpeas.core.admin.service.OrganizationController;
    import org.silverpeas.core.admin.service.OrganizationControllerProvider;
    import org.silverpeas.core.admin.user.model.User;
    import org.silverpeas.core.comment.model.Comment;
    import org.silverpeas.core.comment.service.CommentService;
    import org.silverpeas.core.comment.service.CommentServiceProvider;
    import org.silverpeas.core.contribution.model.ContributionIdentifier;
    import org.silverpeas.core.contribution.model.ContributionModel;
    import org.silverpeas.core.contribution.model.I18nContribution;
    import org.silverpeas.core.contribution.model.SilverpeasContent;
    import org.silverpeas.core.contribution.model.Thumbnail;
    import org.silverpeas.core.contribution.model.WithPermanentLink;
    import org.silverpeas.core.contribution.model.WithThumbnail;
    import org.silverpeas.core.contribution.publication.model.CompletePublication;
    import org.silverpeas.core.contribution.publication.model.Location;
    import org.silverpeas.core.contribution.publication.model.PublicationDetail;
    import org.silverpeas.core.contribution.publication.model.PublicationPK;
    import org.silverpeas.core.contribution.publication.model.PublicationPath;
    import org.silverpeas.core.contribution.publication.service.PublicationService;
    import org.silverpeas.core.i18n.ResourceTranslation;
    import org.silverpeas.core.node.model.NodePK;
    import org.silverpeas.core.pdc.pdc.model.ClassifyPosition;
    import org.silverpeas.core.pdc.pdc.model.PdcException;
    import org.silverpeas.core.pdc.pdc.service.PdcManager;
    import org.silverpeas.core.security.authorization.NodeAccessControl;
    import org.silverpeas.core.silverstatistics.access.service.StatisticService;
    import org.silverpeas.kernel.bundle.ResourceLocator;
    import org.silverpeas.core.util.URLUtil;
    import org.silverpeas.kernel.logging.SilverLogger;

    import java.io.Serial;
    import java.util.Collection;
    import java.util.Collections;
    import java.util.Date;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.Optional;
    import java.util.function.Predicate;

    /**
     * A publication as defined in a Kmelia component. A publication in Kmelia is a publication that is
     * always in a given topic (the father of the publication), and that can be positioned in the PDC,
     * can have attachments, and can be commented.
     * <p>
     * In Kmelia, a publication can be in one or more topics. In such a case, a Kmelia publication that
     * is in a topic other that the original one is said to be an alias of the publication in the
     * original topic: any changes are done in the true publication.
     * </p>
     */
    public class KmeliaPublication
        implements I18nContribution, SilverpeasContent, WithPermanentLink, WithThumbnail {

      @Serial
      private static final long serialVersionUID = 4861635754389280167L;
      private PublicationDetail detail;
      private CompletePublication completeDetail;
      private final PublicationPK pk;
      private boolean read = false;
      private Location location;

      private KmeliaPublication(PublicationPK id) {
        this.pk = id;
      }

      /**
       * Gets the Kmelia publication with the specified primary key identifying it uniquely. If no such
       * publication exists with the specified key, then the runtime exception {@link
       * KmeliaRuntimeException} is thrown. The publication is by default the original one and hence not
       * an alias.
       * @param pk the primary key of the publication to get.
       * @return the Kmelia publication matching the primary key.
       */
      public static KmeliaPublication withPK(final PublicationPK pk) {
        KmeliaPublication publication = new KmeliaPublication(pk);
        publication.loadPublicationDetail();
        return publication;
      }

      /**
       * Gets the Kmelia publication with the specified primary key and that is in the topic specified
       * by its primary key. If no such publication exists with the specified key or if the publication
       * has no such father, then the runtime exception {@link KmeliaRuntimeException} is thrown.
       * @param pk the primary key of the publication to get.
       * @param fatherPk the primary key of the topic that is the father of the publication.
       * @return the Kmelia publication matching the primary key.
       */
      public static KmeliaPublication withPK(final PublicationPK pk, final NodePK fatherPk) {
        KmeliaPublication publication = withPK(pk);
        publication.setFather(fatherPk, null);
        return publication;
      }

      private void setFather(final NodePK fatherPk, final Map<String, List<Location>> locationCache) {
        location = findLocation(fatherPk, locationCache);
        if (location != null) {
          getDetail().setAlias(location.isAlias());
        }
      }

      private Location findLocation(final NodePK pk, final Map<String, List<Location>> locationCache) {
        final Predicate<Location> predicate;
        final String nodeIMsg;
        if (pk != null) {
          predicate = pk::equals;
          nodeIMsg = " in node " + pk.getId() + "(Kmelia " + pk.getInstanceId() + ")";
        } else {
          predicate = l -> !l.isAlias();
          nodeIMsg = "";
        }
        final PublicationPK mainPubPk =
            getDetail().isClone() ? getDetail().getClonePK() : getDetail().getPK();
        return getAllLocations(mainPubPk, locationCache).stream()
            .filter(predicate)
            .findFirst()
            .orElseGet(() -> {
              if (pk == null || !KmeliaHelper.isKmax(pk.getInstanceId())) {
                throw new KmeliaRuntimeException(
                    "Unable to find the location of the publication " + getId() + nodeIMsg);
              }
              return null;
            });
      }

      private Collection<Location> getAllLocations(final PublicationPK mainPubPk,
          Map<String, List<Location>> locationCache) {
        if (locationCache != null) {
          return locationCache.getOrDefault(mainPubPk.getId(), Collections.emptyList());
        }
        return PublicationService.get()
            .getAllLocations(mainPubPk);
      }

      public Location getLocation() {
        if (location == null) {
          location = findLocation(null, null);
        }
        return location;
      }

      /**
       * Gets the Kmelia publication from the specified publication detail. The publication is by
       * default the original one and not an alias.
       * @param detail the detail about the publication to get.
       * @return the Kmelia publication matching the specified publication detail.
       */
      public static KmeliaPublication fromDetail(final PublicationDetail detail) {
        KmeliaPublication publication = new KmeliaPublication(detail.getPK());
        publication.setPublicationDetail(detail);
        return publication;
      }

      /**
       * Gets the Kmelia publication in the given topic from the specified publication detail.
       * @param detail the detail about the publication to get.
       * @param fatherPK the primary key of the topic that is father of the publication to get.
       * @return the Kmelia publication matching the specified publication detail.
       */
      public static KmeliaPublication fromDetail(final PublicationDetail detail,
          final NodePK fatherPK) {
        return fromDetail(detail, fatherPK, null);
      }

      /**
       * Gets the Kmelia publication in the given topic from the specified publication detail.
       * @param detail the detail about the publication to get.
       * @param fatherPK the primary key of the topic that is father of the publication to get.
       * @param locationCache cache of locations already loaded.
       * @return the Kmelia publication matching the specified publication detail.
       */
      public static KmeliaPublication fromDetail(final PublicationDetail detail, final NodePK fatherPK,
          final Map<String, List<Location>> locationCache) {
        final KmeliaPublication publication = new KmeliaPublication(detail.getPK());
        publication.setPublicationDetail(detail);
        publication.setFather(fatherPK, locationCache);
        return publication;
      }

      /**
       * Gets the Kmelia publication from the specified complete publication detail. The publication is
       * by default the original one and not an alias.
       * @param detail the complete detail about the publication to get.
       * @return the Kmelia publication matching the specified complete publication detail.
       */
      public static KmeliaPublication aKmeliaPublicationFromCompleteDetail(
          final CompletePublication detail) {
        KmeliaPublication publication = new KmeliaPublication(detail.getPublicationDetail()
            .getPK());
        publication.setPublicationCompleteDetail(detail);
        return publication;
      }

      public boolean isRead() {
        return this.read;
      }

      public void setAsRead() {
        this.read = true;
      }

      /**
       * Is this publication an alias of an existing Kmelia publication?
       * @return true if this publication is an alias, false otherwise.
       */
      public boolean isAlias() {
        return getDetail().isAlias();
      }

      /**
       * Is this publication visible?
       * @return true if this publication is visible, false otherwise.
       */
      public boolean isVisible() {
        return getDetail().isVisible();
      }

      /**
       * Gets the primary key of this publication.
       * @return the publication primary key.
       */
      public PublicationPK getPk() {
        return pk;
      }

      /**
       * Gets the unique identifier of this publication.
       * @return the unique identifier of this publication.
       */
      @Override
      public String getId() {
        return pk.getId();
      }

      /**
       * Gets the complete URL at which this publication is located.
       * @return the publication URL.
       */
      public String getURL() {
        String defaultURL = getOrganizationController().getDomain(getCreator().getDomainId())
            .getSilverpeasServerURL();
        String serverURL = ResourceLocator.getGeneralSettingBundle()
            .getString("httpServerBase", defaultURL);
        return serverURL + URLUtil.getSimpleURL(URLUtil.URL_PUBLI, getPk().getId());
      }

      /**
       * Gets the details about this publication.
       * @return the publication details.
       */
      public PublicationDetail getDetail() {
        if (detail == null) {
          loadPublicationDetail();
        }
        return detail;
      }

      /**
       * Gets the complete detail about this publication.
       * @return the publication complete details.
       */
      public CompletePublication getCompleteDetail() {
        if (completeDetail == null) {
          setPublicationCompleteDetail(getKmeliaService().getCompletePublication(pk));
        }
        return completeDetail;
      }

      /**
       * Gets the creator of this publication (the initial author).
       * @return the detail about the creator of this publication.
       */
      @Override
      public User getCreator() {
        String creatorId = getDetail().getCreatorId();
        return User.getById(creatorId);
      }

      /**
       * Gets the user that has lastly modified this publication. He's the last one that has worked on
       * this publication. If this publication was not modified since its creation, the creator is
       * returned as he's the last user that has worked on this publication.
       * @return the detail about the last modifier of this publication.
       */
      @Override
      public User getLastUpdater() {
        User lastModifier;
        String modifierId = getDetail().getUpdaterId();
        if (modifierId == null) {
          lastModifier = getCreator();
        } else {
          lastModifier = User.getById(modifierId);
        }
        return lastModifier;
      }

      /**
       * Gets the comments on this publication.
       * @return an unmodifiable list with the comments on this publication.
       */
      public List<Comment> getComments() {
        return Collections.unmodifiableList(
            getCommentService().getAllCommentsOnResource(PublicationDetail.getResourceType(),
                new ResourceReference(pk)));
      }

      /**
       * Gets the positions in the PDC of this publication.
       * @return an unmodifiable list with the PDC positions of this publication.
       */
      public List<ClassifyPosition> getPDCPositions() {
        int silverObjectId = getKmeliaService().getSilverObjectId(pk);
        try {
          return PdcManager.get()
              .getPositions(silverObjectId, pk.getInstanceId());
        } catch (PdcException e) {
          throw new KmeliaRuntimeException(e);
        }

      }

      public int getNbAccess() {
        try {
          return getStatisticService().getCount(new ResourceReference(detail.getPK()), 1,
              "Publication");
        } catch (Exception e) {
          SilverLogger.getLogger(this)
              .error(e);
        }
        return -1;
      }

      @Override
      public boolean equals(Object obj) {
        if (obj == null) {
          return false;
        }
        if (getClass() != obj.getClass()) {
          return false;
        }
        final KmeliaPublication other = (KmeliaPublication) obj;
        return Objects.equals(this.pk, other.pk);
      }

      @Override
      public int hashCode() {
        int hash = 7;
        hash = 67 * hash + (this.pk != null ? this.pk.hashCode() : 0);
        return hash;
      }

      private void setPublicationDetail(final PublicationDetail detail) {
        if (detail == null) {
          throw new KmeliaRuntimeException(
              SilverpeasExceptionMessages.failureOnGetting("publication detail", getId()));
        }
        this.detail = detail;
      }

      private void setPublicationCompleteDetail(final CompletePublication detail) {
        if (this.detail == null) {
          setPublicationDetail(detail.getPublicationDetail());
        }
        this.completeDetail = detail;
      }

      private KmeliaService getKmeliaService() {
        try {
          return KmeliaService.get();
        } catch (Exception e) {
          throw new KmeliaRuntimeException(e);
        }
      }

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

      private CommentService getCommentService() {
        return CommentServiceProvider.getCommentService();
      }

      private OrganizationController getOrganizationController() {
        return OrganizationControllerProvider.getOrganisationController();
      }

      private void loadPublicationDetail() {
        setPublicationDetail(getKmeliaService().getPublicationDetail(pk));
      }

      @Override
      public String getComponentInstanceId() {
        return getDetail().getInstanceId();
      }

      @Override
      public String getSilverpeasContentId() {
        return getDetail().getSilverpeasContentId();
      }

      @Override
      public Date getCreationDate() {
        return getDetail().getCreationDate();
      }

      @Override
      public Date getLastUpdateDate() {
        return getDetail().getLastUpdateDate();
      }

      @Override
      public String getTitle() {
        return getDetail().getTitle();
      }

      @Override
      public String getDescription() {
        return getDetail().getDescription();
      }

      @Override
      public String getContributionType() {
        return getDetail().getContributionType();
      }

      @SuppressWarnings({"unchecked"})
      @Override
      public Optional<PublicationPath> getResourcePath() {
        return getDetail().getResourcePath();
      }

      /**
       * Is the specified user can access this publication?
       * <p>
       * A user can access a publication if he has enough rights to access both the Kmelia instance in
       * which is managed this publication and the topics to which this publication belongs to.
       * @param user a user in Silverpeas.
       * @return true if the user can access this publication, false otherwise.
       */
      @Override
      public boolean canBeAccessedBy(final User user) {
        return getDetail().canBeAccessedBy(user);
      }

      /**
       * Get the number of comments on this publication
       * @return the number.
       */
      public int getNumberOfComments() {
        return getCommentService().getCommentsCountOnResource(PublicationDetail.getResourceType(),
            new ResourceReference(getPk()));
      }

      /**
       * Gets the original location of this alias according to the access right the of the specified
       * user. If this location isn't an alias, then returns itself. If the user has no access right to
       * the original location, then returns nothing. If no original location can be found, whatever the
       * access right of the user, a {@link KmeliaRuntimeException} is thrown.
       * @param userId the unique identifier of the user for which the access right on the original
       * location has to be checked.
       * @return either the original location (itself if this location is already the original one) or
       * nothing whether the given user has no access right on it.
       */
      public Optional<Location> getOriginalLocation(String userId) {
        final Location originalLocation = findLocation(null, null);
        if (originalLocation != null && NodeAccessControl.get()
            .isUserAuthorized(userId, originalLocation)) {
          return Optional.of(originalLocation);
        } else {
          return Optional.empty();
        }
      }

      public ValidatorsList getValidators() {
        return getKmeliaService().getAllValidators(getPk());
      }

      @Override
      public String getPermalink() {
        return detail.getPermalink();
      }

      @Override
      public Thumbnail getThumbnail() {
        return detail.getThumbnail();
      }

      @Override
      public ContributionIdentifier getIdentifier() {
        return detail.getIdentifier();
      }

      @Override
      public ResourceTranslation getTranslation(final String language) {
        return detail.getTranslation(language);
      }

      @Override
      public ContributionModel getModel() {
        return detail.getModel();
      }
    }