All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.cq.social.commons.comments.api.AbstractComment Maven / Gradle / Ivy

There is a newer version: 6.5.21
Show newest version
/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2012 Adobe Systems Incorporated
 *  All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 **************************************************************************/
package com.adobe.cq.social.commons.comments.api;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Map.Entry;

import javax.jcr.RepositoryException;
import javax.jcr.Session;

import com.adobe.cq.social.commons.CommentException;
import com.adobe.cq.social.commons.comments.impl.CommentImpl;
import com.adobe.cq.social.scf.core.BaseQueryRequestInfo;
import com.adobe.cq.social.commons.comments.impl.TransientPropertyConstants;
import com.adobe.cq.social.commons.comments.impl.TransientPropertyHelper;
import com.day.cq.wcm.api.Page;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.SyntheticResource;
import org.apache.sling.api.resource.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.cq.social.commons.Attachment;
import com.adobe.cq.social.commons.CommentSystem;
import com.adobe.cq.social.commons.CommentUtil;
import com.adobe.cq.social.commons.comments.states.internal.State;
import com.adobe.cq.social.commons.comments.states.internal.StateMachine;
import com.adobe.cq.social.commons.comments.listing.CommentSocialComponentList;
import com.adobe.cq.social.commons.comments.listing.CommentSocialComponentListProvider;
import com.adobe.cq.social.commons.comments.listing.CommentSocialComponentListProviderManager;
import com.adobe.cq.social.commons.moderation.api.FlagReason;
import com.adobe.cq.social.commons.tagging.SocialTagManager;
import com.adobe.cq.social.scf.ClientUtilities;
import com.adobe.cq.social.scf.CollectionPagination;
import com.adobe.cq.social.scf.QueryRequestInfo;
import com.adobe.cq.social.scf.SocialComponent;
import com.adobe.cq.social.scf.SocialComponentFactory;
import com.adobe.cq.social.scf.SocialComponentFactoryManager;
import com.adobe.cq.social.scf.User;
import com.adobe.cq.social.scf.core.BaseSocialComponent;
import com.adobe.cq.social.scf.core.CollectionSortedOrder;
import com.adobe.cq.social.srp.SocialResource;
import com.adobe.cq.social.srp.SocialResourceProvider;
import com.adobe.cq.social.srp.utilities.internal.InternalSocialResourceUtilities;
import com.adobe.cq.social.tally.Voting;
import com.adobe.cq.social.tally.client.api.Response;
import com.adobe.cq.social.tally.client.api.TallyException;
import com.adobe.cq.social.tally.client.api.Vote;
import com.adobe.cq.social.translation.TranslationResults;
import com.adobe.cq.social.translation.TranslationSCFUtil;
import com.adobe.cq.social.translation.TranslationUtil;
import com.adobe.cq.social.ugcbase.CollabUser;
import com.adobe.cq.social.ugcbase.SocialUtils;
import com.adobe.cq.social.ugcbase.core.SocialResourceUtils;
import com.adobe.granite.security.user.UserProperties;
import com.day.cq.tagging.TagConstants;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;

public abstract class AbstractComment extends BaseSocialComponent implements
    Comment {
    /** Logger for this class. */
    private static final Logger LOG = LoggerFactory.getLogger(AbstractComment.class);

    private static final String UNKNOWN_USER = "Unknown";
    private static final String JCR_DESCRIPTION_PROP = "jcr:description";
    private static final String JCR_TITLE_PROP = "jcr:title";
    private static String USERS_HOME_UGC = "/home/users/";

    /** local variables. */
    protected final CommentSocialComponentList replies;
    private final User author;
    private final Map attachments;
    private final Calendar created;
    private final String message;
    private final String parentPath;
    private final String resourceType;
    private final boolean isTopLevel;
    private final boolean isApproved;
    private boolean isDraft = false;
    private final State state;
    private boolean isScheduled = false;
    private final Calendar publishDate;
    private boolean canEdit = false;
    private boolean canDelete = false;
    private boolean userIsModerator = false;
    private boolean currentUserFlagged = false;
    private String currentUserFlagText = null;
    private String translationAttribution = null;
    private final QueryRequestInfo queryInfo;
    private final boolean isVisible;
    private final boolean isClosed;
    private final boolean isPinned;
    private final boolean isFeatured;
    private final boolean doDisplayTranslation;
    private final boolean displayReplyButton;
    private T configuration;
    private final ModeratorActions moderatorActions;
    private final ModeratorStatus moderatorStatus;
    private final String votingRoot;
    private boolean useFlagReasonList = true;
    private boolean useFlagReasons = false;
    private boolean useReferrerUrl;
    private List flagReasons;
    private CommentSocialComponentListProviderManager commentListProviderManager;
    private final com.adobe.cq.social.commons.Comment comment;
    private SocialComponent parentComponent;
    private SocialComponent sourceComponent;
    private PageInfo pageInfo;
    private String descriptionTranslation = null;
    private String titleTranslation = null;
    private String displayTranslation = null;
    private int MAX_ERROR_MESSAGE_LENGTH = 1000; // max message length that we log in the case filterHTML failing.

    private final String MODERATORS_ROLE = "moderators";
    private final String OWNER_ROLE = "owner";
    private final String EDIT_OPERATION = "Edit";
    private final String DELETE_OPERATION = "Delete";
    private final String PROP_STATE = "state_s";
    private final String ERROR_HTML_FILTER_MESSAGE =
        "There was something wrong with this post.  Please contact your administrator to fix the issue.";
    private final String FILTER = "filter";
    private final String PAGE = "page has";
    private final String RESOURCE_COMMENTS = "se_social/se_discussion/";
    private final String CATALOG = "catalog.";
    private final String LEARNINGPATH = "learningpath";
    private final String RESOURCE = "resource";

    private String filterFromRequest = null;

    /**
     * Construct a comment for the specified resource and client utilities.
     * @param resource the specified resource
     * @param clientUtils the client utilities instance
     * @param commentListProviderManager list manager to use for listing content
     * @throws RepositoryException if an error occurs
     */
    public AbstractComment(final Resource resource, final ClientUtilities clientUtils,
        final CommentSocialComponentListProviderManager commentListProviderManager) throws RepositoryException {
        this(resource, clientUtils, QueryRequestInfo.DEFAULT_QUERY_INFO_FACTORY.create(), commentListProviderManager);
    }

    /**
     * Constructor of a comment.
     * @param resource the specified {@link com.adobe.cq.social.commons.Comment}
     * @param clientUtils the client utilities instance
     * @param queryInfo the query info.
     * @param commentListProviderManager list manager to use for listing content
     * @throws RepositoryException if an error occurs
     */
    public AbstractComment(final Resource resource, final ClientUtilities clientUtils,
        final QueryRequestInfo queryInfo, final CommentSocialComponentListProviderManager commentListProviderManager)
        throws RepositoryException {
        super(resource, clientUtils);
        this.commentListProviderManager = commentListProviderManager;
        comment = resource.adaptTo(com.adobe.cq.social.commons.Comment.class);
        author = this.clientUtils.getUser(comment.getAuthor().getId(), comment.getResource().getResourceResolver());
        attachments = comment.getAttachments();
        created = comment.getCreated();
        message = comment.getMessage();
        parentPath = comment.getComponent().getPath();
        isTopLevel = comment.isTopLevel();

        //This is needed for deeplinking of repliess
        if (queryInfo.getPredicates().containsKey(FILTER) && (queryInfo.getPredicates().get(FILTER)[0].startsWith(PAGE))) {
            filterFromRequest = queryInfo.getPredicates().get(FILTER)[0].replace(PAGE, "").trim();
            this.queryInfo = new BaseQueryRequestInfo(false, new HashMap(), queryInfo.getPagination(), queryInfo.getSortOrder(), queryInfo.getSortBy());
        } else {
            this.queryInfo = queryInfo;
        }

        flagReasons = new ArrayList();

        final Session session = resource.getResourceResolver().adaptTo(Session.class);
        final String userId = session.getUserID();

        final CommentSystem cs = comment.getCommentSystem();
        // Comment is approved or it doesn't need to be because it's not moderated.
        isApproved = !comment.isSpam() && (comment.isApproved() || !cs.isModerated());
        userIsModerator = clientUtils.getSocialUtils().hasModeratePermissions(resource);
        isDraft = comment.getProperty(com.adobe.cq.social.commons.Comment.PROP_IS_DRAFT, false);
        // to get the state of the comment
        // TODO: adaptor factory
        final String currentState = comment.getProperty(PROP_STATE, "");
        final StateMachine stateMachine = resource.adaptTo(StateMachine.class);
        state = stateMachine == null ? null : stateMachine.getState(currentState);
        isScheduled = comment.getProperty(com.adobe.cq.social.commons.Comment.PROP_IS_SCHEDULED, false);
        publishDate = comment.getProperty(com.adobe.cq.social.commons.Comment.PROP_PUBLISH_DATE, Calendar.class);
        final boolean userIsLoggedIn =
            clientUtils == null ? !(userId == null || userId.equalsIgnoreCase(CollabUser.ANONYMOUS)) : !clientUtils
                .userIsAnonymous();

        isClosed = comment.isClosed();
        isPinned = comment.getProperty(com.adobe.cq.social.commons.Comment.PROP_PINNED, false);
        isFeatured = comment.getProperty(com.adobe.cq.social.commons.Comment.PROP_FEATURED, false);
        useReferrerUrl = cs.getProperty(PROP_USE_REFERRER_URL, Boolean.FALSE);
        configuration = createConfiguration(resource, cs.getResource());
        // TODO: the complete logic should be inside state machine
        final boolean canModEdit = state == null ? true : state.canPerformOperation(MODERATORS_ROLE, EDIT_OPERATION);
        final boolean canOwnerEdit = state == null ? true : state.canPerformOperation(OWNER_ROLE, EDIT_OPERATION);
        final boolean canModDelete =
            state == null ? true : state.canPerformOperation(MODERATORS_ROLE, DELETE_OPERATION);
        final boolean canOwnerDelete = state == null ? true : state.canPerformOperation(OWNER_ROLE, DELETE_OPERATION);
        if ((userIsModerator && canModEdit) || (userIsOwner() && userIsLoggedIn && canOwnerEdit) && !isClosed) {
            canEdit = true;
        }

        if ((userIsModerator && canModDelete)
                || (userIsOwner() && userIsLoggedIn && canOwnerDelete && cs.allowsDelete()) && !isClosed) {
            canDelete = true;
        }

        // A comment is visible to moderators, and to others if it's not spam, not hidden from flagging,
        // and not premoderated and not yet approved.
        if (userIsModerator || (!comment.isSpam() && !comment.isFlaggedHidden() && isApproved && !isDraft)) {
            isVisible = true;
        } else {
            isVisible = false;
        }
        displayReplyButton = canUserReply(userIsLoggedIn, cs, session);

        moderatorStatus = (userIsModerator) ? new ModeratorStatusImpl(comment, cs) : null;

        if (cs.allowsFlagging()) {
            useFlagReasonList = cs.useFlagReasonList();
            useFlagReasons = useFlagReasonList || cs.allowCustomFlagReason();
            final Response response = getFlagResponseForUser(userId);
            if (response != null && response.getResponseValue() != null) { // this user DID flag this content
                currentUserFlagged = true;
                if (useFlagReasons) {
                    final Resource responseResource = response.getResource();
                    final ValueMap resourceProperties = responseResource.adaptTo(ValueMap.class);

                    // Should not be null, but ensure it's not anyways.
                    currentUserFlagText =
                        resourceProperties.get(com.adobe.cq.social.commons.Comment.PROP_FLAG_REASON, "");
                }
            }

            if (userIsModerator && useFlagReasons) {
                flagReasons = listFlagReasons(resource, clientUtils);
            }
        }

        this.moderatorActions = new ModeratorActionsImpl(comment, cs, userIsLoggedIn);

        resourceType = comment.getResource().getResourceType();

        final QueryRequestInfo repliesListInfo = QueryRequestInfo.DEFAULT_QUERY_INFO_FACTORY.create(this.queryInfo);
        final CollectionPagination currentPagination = repliesListInfo.getPagination();
        CollectionPagination repliesPagination;

        if (configuration.isDoNotGetRepliesOnListingPage() && resource != null && TransientPropertyHelper.getTransientProperty(resource,TransientPropertyConstants.TRANSIENT_PROPERTY_COLLECTION, false)) {
            repliesPagination = new CollectionPagination(0, 0, 0, currentPagination.getSortIndex(), this.configuration.getPageSize());
        }else if (currentPagination.getEmbedLevel() != CollectionPagination.DEFAULT_EMBED_LEVEL) {
            // don't fetch nested replies
            repliesPagination =
                new CollectionPagination(0, 0, currentPagination.getEmbedLevel() + 1,
                    currentPagination.getSortIndex(), this.configuration.getPageSize());
        } else {
            repliesPagination =
                new CollectionPagination(currentPagination.getOffset(),
                    queryInfo.getPagination() == CollectionPagination.DEFAULT_PAGINATION ? this.configuration
                        .getPageSize() : currentPagination.getSize(),
                    currentPagination.getEmbedLevel() + 1, currentPagination.getSortIndex(),
                    this.configuration.getPageSize());
        }
        repliesListInfo.setPagination(repliesPagination);
        repliesListInfo.setSortOrder(CollectionSortedOrder.DEFAULT_ORDER);

        replies = getReplies(resource, this, this.filterFromRequest, repliesListInfo);

        final Resource component = cs.getResource();
        if (configuration.isVotingAllowed()) // set voting root only if its allowed for a SCF component
            votingRoot = CommentUtil.getVotingRoot(component);
        else
            votingRoot = "";

        // if this is a search, prefetches are done in SearchComponent and no need to show translate button
        if (!queryInfo.isQuery()
                && resource.getPath().indexOf(
                    clientUtils.getSocialUtils().getStorageConfig(resource).getAsiPath() + USERS_HOME_UGC) == -1) {
            prefetchResources(resource.getResourceResolver(), clientUtils, cs);

            // call the function from TranslationUtil to determine weather show the translation button.
            doDisplayTranslation =
                TranslationUtil.doDisplayTranslation(resource.getResourceResolver(), resource, clientUtils);
        } else {
            doDisplayTranslation = false;
        }

        pageInfo = new PageInfo(this, clientUtils, repliesListInfo.getPagination());

        if ((this.queryInfo.isTranslationRequest() && this.queryInfo.getPagination().getEmbedLevel() == 0)
                || (clientUtils.getRequest() != null && TranslationSCFUtil.isSmartRenderingOn(resource, clientUtils))) {
            final TranslationResults results = TranslationSCFUtil.getTranslationSCF(resource, clientUtils);
            if (results != null) {
                final Map translations = results.getTranslation();
                this.displayTranslation = results.getDisplay();

                this.translationAttribution = results.getAttribution();
                if (!translations.isEmpty()) {
                    this.descriptionTranslation = translations.get(JCR_DESCRIPTION_PROP);
                    this.titleTranslation = translations.get(JCR_TITLE_PROP);
                }
            }
        }
    }

    /* Returns the index of a reply within all replies of a given comment for given conditions */
    private int getChildIndex(final String commentToCheck, final int size, final List> sortFields) throws CommentException {
        int replyIndex = 0;
        if (clientUtils != null) {
            final String pathToCheck = clientUtils.getCommonStorePath(resource);
            final SocialResourceProvider srp = SocialResourceUtils.getSocialResource(resource).getResourceProvider();
            if (srp != null) {
                replyIndex = srp.getCommentIndex(pathToCheck, com.adobe.cq.social.commons.Comment.RESOURCE_TYPE, resource.getResourceResolver(),
                        pathToCheck + "/" + commentToCheck.split("/")[0], true);
            }
        }
        if (replyIndex < 0) {
            LOG.warn("Unable to get comment index. Fail to provide direct link to comment");
            replyIndex = 0; //let's open 1st page
        }
        return replyIndex;
    }

    /* This function just handles first level reply to a top level post. We will have to write separate function for nested levels */
    private CommentSocialComponentList getReplies(final Resource resource, final AbstractComment abstractComment, final String filterFromRequest, final QueryRequestInfo repliesListInfo) {
        CollectionPagination repliesPagination = repliesListInfo.getPagination();
        if (filterFromRequest != null) {
            int replyIndex = getChildIndex(filterFromRequest, this.configuration.getPageSize(), repliesListInfo.getSortFields());
            int pageNo = replyIndex/this.configuration.getPageSize() ;
            repliesPagination = new CollectionPagination(pageNo * this.configuration.getPageSize(), this.configuration.getPageSize(), 1,
                    repliesPagination.getSortIndex(), this.configuration.getPageSize());
            repliesListInfo.setPagination(repliesPagination);

        }

        CommentSocialComponentListProvider listProvider = this.commentListProviderManager.getCommentSocialComponentListProvider(resource, repliesListInfo);
        CommentSocialComponentList replies = listProvider.getCommentSocialComponentList(abstractComment, repliesListInfo, clientUtils);

        return replies;
    }

    /**
     * @param userIsLoggedIn - true id the current request is from a logged in user
     * @param cs - the {@link CommentSystem} that this {@link Comment} belongs to
     * @param session - the {@link Session} associated with the current resolver
     * @return true if the user can reply, false otherwise
     */
    protected boolean canUserReply(final boolean userIsLoggedIn, final CommentSystem cs, final Session session) {
        boolean result = false;
        if (userIsLoggedIn) {
            final SocialUtils socialUtils = clientUtils.getSocialUtils();
            if (socialUtils != null) {
                final String aclPath = socialUtils.resourceToACLPath(cs.getResource());
                final boolean mayPost =
                    clientUtils.getSocialUtils().canAddNode(session,
                        aclPath == null ? CommentSystem.PATH_UGC : aclPath);
                result = cs.allowsReplies() && !isClosed && mayPost;
            } else {
                result = false;
            }
        } else {
            result = false;
        }
        return result;
    }

    private void prefetchResources(final ResourceResolver resolver, final ClientUtilities clientUtils,
        final CommentSystem cs) {

        if (!SocialResourceUtils.isSocialResource(resource)) {
            return;
        }

        final List paths = new ArrayList(3);
        if (clientUtils.isTranslationServiceConfigured(resource)) {
            paths.add(resource.getPath() + "/" + TranslationUtil.TRANSLATION_NODE_NAME);
            if (!isTopLevel) {
                final ValueMap vm = resource.adaptTo(ValueMap.class);
                if (vm != null) {
                    final String s = vm.get(InternalSocialResourceUtilities.PN_PARENTID, String.class);
                    if (s != null) {
                        paths.add(s + "/" + TranslationUtil.TRANSLATION_NODE_NAME);
                    }
                }
            }
        }

        if (configuration.isVotingAllowed()) {
            paths.add(resource.getPath() + "/" + votingRoot);

            if (!clientUtils.userIsAnonymous()) {
                paths.add(resource.getPath() + "/" + votingRoot + "/" + clientUtils.getAuthorizedUserId());
            }
        }

        if (!paths.isEmpty()) {
            final SocialResourceProvider srp = SocialResourceUtils.getSocialResource(resource).getResourceProvider();
            srp.getResources(resolver, paths);
        }
    }

    /**
     * Obtain the configuration for this comment.
     * @param resource The comment resource
     * @param commentSystem Resource
     * @return T type
     */
    protected T createConfiguration(final Resource resource, final Resource commentSystem) {
        if (ResourceUtil.isNonExistingResource(commentSystem) && clientUtils != null) {
            final ValueMap vm = this.clientUtils.getDesignProperties(commentSystem, CommentSystem.PROP_RESOURCE_TYPE);
            return (T) new AbstractCommentCollectionConfiguration(vm);
        }
        return (T) new AbstractCommentCollectionConfiguration(commentSystem);
    }

    @Override
    public T getConfiguration() {
        return configuration;
    }

    private boolean userIsOwner() {
        // TODO CollabUtil.isResourceOwner(resource) is supposed to do this, but it always treats moderators as owners
        final Session session = resource.getResourceResolver().adaptTo(Session.class);
        final ValueMap map = resource.adaptTo(ValueMap.class);
        String resourceAuthorID = map.get(CollabUser.PROP_NAME, String.class);
        final String composedByID = map.get(com.adobe.cq.social.commons.Comment.PROP_AUTHORIZABLE_ID, String.class);
        if (StringUtils.isEmpty(resourceAuthorID)) {
            // in case the resource has no userIdentifier property, for example calendar event
            resourceAuthorID = map.get(com.day.cq.commons.jcr.JcrConstants.JCR_LAST_MODIFIED_BY, String.class);
        }
        return StringUtils.equals(session.getUserID(), resourceAuthorID)
                || StringUtils.equals(session.getUserID(), composedByID);
    }

    /**
     * Get the path for the current version of the flag tally. This may or may not exist.
     * @return String containing the path, or null if the resource has no returnable properties.
     */
    private String getFlagsPath() {

        final ValueMap props = resource.adaptTo(ValueMap.class);
        if (props == null) {
            LOG.warn("Unable to adapt resource {} to ValueMap.");
            return null;
        }

        final int flagAllowCount = props.get(com.adobe.cq.social.commons.Comment.PROP_FLAG_ALLOW_COUNT, -1);
        if (flagAllowCount < 0) {
            // No flags exist yet, so no path to return.
            return null;
        }

        return resource.getPath() + com.adobe.cq.social.commons.Comment.FLAG_PATH_PREFIX + flagAllowCount;
    }

    /**
     * Return response of current user to this comment. If this comment was not flagged by this user (or was last
     * unflagged), return null.
     * @return Response
     * @throws RepositoryException
     */
    private Response getFlagResponseForUser(final String currentUserId) throws RepositoryException {
        // find the vote and text provided by the current user, if any.
        final ResourceResolver resolver = resource.getResourceResolver();

        final String flagsPath = getFlagsPath();
        if (flagsPath == null) {
            return null;
        }
        final Resource flagResource = resolver.resolve(flagsPath);
        if (ResourceUtil.isNonExistingResource(flagResource)) {
            return null;
        }

        final Voting voting = flagResource.adaptTo(Voting.class);
        Response response = null;
        try {
            response = voting.getUserResponse(currentUserId);
        } catch (final RepositoryException e) {
            LOG.error("Repository Exception getting voting response for user {}.", currentUserId);
        } catch (final TallyException e) {
            LOG.error("Tally Exception getting voting response for user {}.", currentUserId);
        }

        return response;
    }

    /**
     * Get a list of flag reasons.
     * @param resource The comment resource
     * @param clientUtils
     * @return The list of flag reasons.
     * @throws RepositoryException
     */
    private List listFlagReasons(final Resource resource, final ClientUtilities clientUtils)
        throws RepositoryException {

        final List reasons = new ArrayList();

        final ResourceResolver resolver = resource.getResourceResolver();
        final String flagsPath = getFlagsPath();
        if (flagsPath == null) {
            return reasons;
        }
        final Resource flagResource = resolver.resolve(flagsPath);

        if (!ResourceUtil.isNonExistingResource(flagResource)) {
            final Voting voting = flagResource.adaptTo(Voting.class);
            if (voting != null) { 
                final Iterator> responses = voting.getResponses(0L);
                while (responses.hasNext()) {
                    final Response response = responses.next();
                    final Resource responseResource = response.getResource();
                    final ValueMap resourceProperties = responseResource.adaptTo(ValueMap.class);
    
                    // Set the flag reason text
                    final String flagReasonText =
                        (String) resourceProperties.get(com.adobe.cq.social.commons.Comment.PROP_FLAG_REASON);
                    final FlagReason flagReason = new FlagReason(flagReasonText);
    
                    // Set the user name
                    final String flagUserId = response.getUserId();
                    final UserProperties userProps = clientUtils.getSocialUtils().getUserProperties(resolver, flagUserId);
    
                    final String flagUser = userProps != null ? userProps.getDisplayName() : UNKNOWN_USER;
                    flagReason.setUser(flagUser);
    
                    // Add the reason to the list
                    reasons.add(flagReason);
                }
            }
        }

        return reasons;
    }

    @Override
    protected List getIgnoredProperties() {
        this.ignoredProperties.add("jcr:.*");
        this.ignoredProperties.add("userIdentifier");
        this.ignoredProperties.add("referer");
        this.ignoredProperties.add("authorizableId");
        this.ignoredProperties.add("authorizableId");
        this.ignoredProperties.add(com.adobe.cq.social.commons.Comment.PROP_PINNED);
        return this.ignoredProperties;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Attachment getAttachment(final String name) {
        return attachments.get(name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public User getAuthor() {
        return author;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getState() {
        return (state == null) ? null : state.getTitle();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map getAttachments() {
        return attachments;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Calendar getCreated() {
        return created;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getResourceType() {
        return resourceType;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getMessage() {
        try {
            return clientUtils.filterHTML(message);
        } catch (Throwable e) {
            // this operation may fail if the message length is too long.
            final String msg =
                (message.length() > MAX_ERROR_MESSAGE_LENGTH) ? message.substring(MAX_ERROR_MESSAGE_LENGTH) : message;
            LOG.error(
                "Failed to filter HTML for the message {}.  Please configure max message length or change your XSS rules for the comment system to fix the issue.",
                msg);
            return ERROR_HTML_FILTER_MESSAGE;
        }
    }

    /**
     * Gets the parent path for the comment.
     * @return a string pointing to the externalized URL to the parent.
     */
    public String getParent() {
        return this.externalizeURL(this.parentPath);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return getUrl();
    }

    /**
     * Set the collection list range. If this is not set, then the return list will start at offset 0 and the default
     * size
     * @param pagination detail information to use
     */
    @Override
    public void setPagination(final CollectionPagination pagination) {
        replies.setPagination(pagination);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setSortedOrder(final CollectionSortedOrder sortedOrder) {
        replies.setSortedOrder(sortedOrder);
    }

    public void setSortFields(final List> sortFields) {
        replies.setSortFields(sortFields);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isTopLevel() {
        return isTopLevel;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isApproved() {
        return isApproved;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean getCanEdit() {
        return canEdit;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean getCanReply() {
        return displayReplyButton;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean getCanDelete() {
        return canDelete;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getTotalSize() {
        return replies.getTotalSize();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List getItems() {
        return replies;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getParentId() {
        final SocialComponent pc = this.getParentComponent();
        if (pc == null) {
            LOG.warn("Could not get Parent SocialComponent for {}", this.getResource().getPath());
            return null;
        }
        return pc.getId().getResourceIdentifier();
    }

    @Override
    public String getSourceComponentId() {
        final SocialComponent sc = this.getSourceComponent();
        if (sc == null) {
            Map props = getProperties();
            if (props.containsKey(SocialUtils.PN_PARENTID)) {
                final Object o = props.get(SocialUtils.PN_PARENTID);
                if (o instanceof String) {
                    return (String) o;
                }
            }
            LOG.warn("Could not get Source SocialComponent for {}", this.getResource().getPath());
            return null;
        }
        return sc.getId().getResourceIdentifier();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean getCanTranslate() {
        return this.doDisplayTranslation;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isUserModerator() {
        return this.userIsModerator;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isVisible() {
        return isVisible;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isClosed() {
        return this.isClosed;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isPinned() {
        return this.isPinned;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isFeatured() {
        return this.isFeatured;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isDraft() {
        return this.isDraft;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isScheduled() {
        return this.isScheduled;
    }

    @Override
    public Calendar getPublishDate() {
        return this.publishDate;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List getFlagReasons() {
        return flagReasons;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getCurrentUserFlagText() {
        return currentUserFlagText;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean getUseFlagReasons() {
        return useFlagReasons;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isFlaggedByUser() {
        return currentUserFlagged;
    }

    private static class SyntheticVotingResource extends SyntheticResource implements SocialResource {

        private final SocialResource parent;

        public SyntheticVotingResource(final ResourceResolver resourceResolver, final SocialResource parentIn,
            final String path, final String resourceType) {
            super(resourceResolver, path, resourceType);
            parent = parentIn;
        }

        @Override
        public SocialResourceProvider getResourceProvider() {
            return parent.getResourceProvider();
        }

        @Override
        public Resource getRootJCRNode() {
            return parent.getRootJCRNode();
        }

        @Override
        public boolean checkPermissions(final String permission) {
            return parent.checkPermissions(permission);
        }

    }

    @Override
    public SocialComponent getVotes() {
        if (configuration.isVotingAllowed()) {
            Resource voteResource = resource.getChild(votingRoot);
            if (voteResource == null || ResourceUtil.isNonExistingResource(voteResource)) {
                // when the voting resource does not exist, create a synthetic resource to work with
                // SocialComponentFactory
                if (SocialResourceUtils.isSocialResource(resource)) {
                    voteResource =
                        new SyntheticVotingResource(resource.getResourceResolver(),
                            SocialResourceUtils.getSocialResource(resource), resource.getPath() + "/" + votingRoot,
                            CommentUtil.getVotingType(comment.getComponent()));
                } else {
                    voteResource =
                        new SyntheticResource(resource.getResourceResolver(), resource.getPath() + "/" + votingRoot,
                            CommentUtil.getVotingType(comment.getComponent()));
                }
            }
            final SocialComponentFactoryManager scfMgr = clientUtils.getSocialComponentFactoryManager();
            if (scfMgr != null) {
                final SocialComponentFactory scf = scfMgr.getSocialComponentFactory(voteResource);
                if (scf != null) {
                    return scf.getSocialComponent(voteResource, clientUtils,
                        QueryRequestInfo.DEFAULT_QUERY_INFO_FACTORY.create());
                }
            }
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ModeratorActions getModeratorActions() {
        return this.moderatorActions;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @JsonInclude(Include.NON_NULL)
    public ModeratorStatus getModeratorStatus() {
        return moderatorStatus;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SocialComponent getParentComponent() {
        if (parentComponent == null) {
            setParentOrSourceComponent(false);
        }
        return parentComponent;
    }

    /**
     * The idea here is to allow us to search a hierarchy of nodes for both the parent and the source at the same
     * time. In this case if we stumble along the source component while looking for the parent we will set it as
     * well. If only the parent is request we won't look higher for the source component, but if we are looking for
     * the source component we can start at the parent ( maybe skipping some nodes ).
     * @param forceSourceComponent
     */
    private void setParentOrSourceComponent(final boolean forceSourceComponent) {
        if (this.parentComponent != null && this.sourceComponent != null) {
            return;
        }
        Resource parentResource;
        if (this.parentComponent != null) {
            parentResource = parentComponent.getResource();
        } else {
            parentResource = this.getResource().getParent();
        }
        while (parentResource != null) {
            if (parentResource.adaptTo(ValueMap.class).containsKey(SocialUtils.PROP_COMPONENT)) {
                final Resource tempParent =
                    parentResource.getResourceResolver().getResource(
                        parentResource.adaptTo(ValueMap.class).get(SocialUtils.PROP_COMPONENT, String.class));
                if (tempParent != null) {
                    parentResource = tempParent;
                } else {
                    // When the component was sling included we don't have any clue where this component actually came
                    // from so we should just use the node that actually had the pointer back to content.
                    sourceComponent = parentComponent = getComponent(parentResource);
                    break;
                }
                // If the resource has a pointer to the root content node then this points us to the source component
                // so if it hasn't been set set it.

                if (sourceComponent == null) {
                    sourceComponent = getComponent(parentResource);
                }
            }
            if (parentComponent == null) {
                parentComponent = getComponent(parentResource);
            }
            if (sourceComponent == null && forceSourceComponent) {
                if (!StringUtils.startsWith(parentResource.getPath(), SocialUtils.PATH_UGC)) {
                    sourceComponent = getComponent(parentResource);
                }
                parentResource = parentResource.getParent();

            } else {
                break;
            }
        }
    }

    private SocialComponent getComponent(final Resource srcResource) {
        if (srcResource == null) {
            return null;
        }
        final SocialComponentFactory parentFactory =
            clientUtils.getSocialComponentFactoryManager().getSocialComponentFactory(srcResource);
        if (parentFactory == null) {
            return null;
        }
        return parentFactory.getSocialComponent(srcResource, clientUtils, queryInfo);

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SocialComponent getSourceComponent() {
        if (sourceComponent == null) {
            setParentOrSourceComponent(true);
        }
        return sourceComponent;
    }

    /**
     * Get the list of replies.
     * @return the children of this comment.
     * @throws RepositoryException is thrown if there an error occurs while fetching the data.
     */
    protected List getComments() throws RepositoryException {
        return replies.getComments();
    }

    /**
     * Get the pagination information for the comment.
     * @return the QueryRequestInfo pagination details
     */
    protected CollectionPagination getPagination() {
        return queryInfo.getPagination();
    }

    /**
     * Get the query info comment.
     * @return the QueryRequestInfo details
     */
    protected QueryRequestInfo getQueryRequestInfo() {
        return queryInfo;
    }

    @Override
    public List getTags() {
        final SocialTagManager tm = getResource().getResourceResolver().adaptTo(SocialTagManager.class);
        com.day.cq.tagging.Tag[] userTags = new com.day.cq.tagging.Tag[0];
        final Locale pageLocale = getPageLocale(this.getResource());
        try {
            userTags = tm.getTags(this.getResource());
        } catch (final Exception e) {
            // Ideally Need to change in JcrTaManagerImpl , FailSafe code for the Forum Post.
            if (userTags != null && userTags.length <= 0) {
                try {
                    if (getProperties().get(TagConstants.PN_TAGS) != null
                            && !getProperties().get(TagConstants.PN_TAGS).getClass().isArray()) {
                        String tag = null;
                        tag = getProperties().get(TagConstants.PN_TAGS).toString();
                        final com.day.cq.tagging.Tag singleTag = tm.resolve(tag);
                        userTags = new com.day.cq.tagging.Tag[1];
                        userTags[0] = singleTag;
                    }
                } catch (final Exception e1) {
                    LOG.error("Error retrieving tags: ", e1);
                }
            }

        }
        final List tags = new ArrayList(userTags.length);
        for (final com.day.cq.tagging.Tag t : userTags) {
            if (t != null) {
                final Tag tag = new Tag() {

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

                    @Override
                    public String getTagId() {
                        return t.getTagID();
                    }

                };
                tags.add(tag);
            }
        }
        return tags;
    }

    /**
     * Method to find out the locale of the page where tags are being pulled for
     * If there is an explicit content page path available, use that else fallback on Default Locale which is always Locale.ENGLISH
     * @param currentResource
     * @return locale
     */
    private Locale getPageLocale(Resource currentResource){
        Locale locale = Locale.ENGLISH;
        if(currentResource != null) {
            Page page =  this.clientUtils.getSocialUtils().getContainingPage(currentResource);
            if(page != null) {
                locale = page.getLanguage(false);

            }
        }
        return locale;

    }
    protected class ModeratorStatusImpl implements ModeratorStatus {
        private final boolean flagged;
        private final boolean approved;
        private final boolean spam;
        private final boolean pending;

        public ModeratorStatusImpl(final com.adobe.cq.social.commons.Comment comment, final CommentSystem cs) {
            flagged = comment.isFlagged();
            spam = comment.isSpam();
            approved = !spam && (comment.isApproved() || !cs.isModerated());
            pending = cs.isModerated() && !comment.isApproved() && !comment.isDenied();
        }

        @Override
        @JsonProperty("isFlagged")
        public boolean isFlagged() {
            return flagged;
        }

        @Override
        @JsonProperty("isApproved")
        public boolean isApproved() {
            return approved;
        }

        @Override
        @JsonProperty("isSpam")
        public boolean isSpam() {
            return spam;
        }

        @Override
        @JsonProperty("isPending")
        public boolean isPending() {
            return pending;
        }
    }

    protected class ModeratorActionsImpl implements ModeratorActions {
        private boolean displayAllowButton = false;
        private boolean displayFlagButton = false;
        private boolean displayDenyButton = false;
        private boolean displayCloseButton = false;
        private boolean displayReviewButton = false;
        private List nextStates = new ArrayList();
        private boolean displayPinButton = false;
        private boolean canMove = false;
        private boolean displayFeaturedLink = false;
        private final String OPERATION_REVIEW_IDEA = "Review Idea";

        public ModeratorActionsImpl(final com.adobe.cq.social.commons.Comment comment, final CommentSystem cs,
            final boolean userIsLoggedIn) {
            final boolean commentApproved = comment.isApproved() || !cs.isModerated();

            // Logic explanation:
            // SHOW the Flag button only if all of these are true
            // The comment is not closed
            // The current user has not flagged it
            // The user is logged in
            // The comment is approved or the comment system is not premoderated
            // The user doesn't own the resource (and is not a mod, as they are considered the owner ??)
            // The comment is not spam
            // The comment has not been flagged enough times to be hidden
            // The comment system allows flagging.
            // HIDE the Flag button if any of them are not true.
            displayFlagButton =
                !isClosed && !currentUserFlagged && userIsLoggedIn && commentApproved && !userIsOwner()
                        && !comment.isSpam() && !comment.isFlaggedHidden() && cs.allowsFlagging();

            displayAllowButton =
                userIsModerator && !isClosed && (comment.isSpam() || comment.isFlagged() || !commentApproved);

            displayDenyButton = userIsModerator && !comment.isSpam() && cs.allowsDeny();
            // close or open button are both covered under displayCloseButton:
            displayPinButton = userIsModerator && isTopLevel && cs.allowsPin();

            // close or open button are both covered under displayCloseButton:
            displayCloseButton = userIsModerator && isTopLevel && cs.allowsClose();
            // show "move" option if user is a moderator and if the comment system is configured to allow moves and if
            // its not closed
            canMove = userIsModerator && isTopLevel && !comment.isClosed() && cs.allowsMove();

            displayFeaturedLink = userIsModerator && isTopLevel && configuration.isFeaturingContentAllowed();

            // change state buttons
            displayReviewButton =
                state == null ? false : userIsModerator && isTopLevel
                        && state.canPerformOperation(MODERATORS_ROLE, OPERATION_REVIEW_IDEA);

            if (userIsModerator && isTopLevel && state != null) {
                nextStates = state.getNextStatesForRole(MODERATORS_ROLE);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean getCanDeny() {
            return this.displayDenyButton;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean getUseFlagReasonList() {
            return useFlagReasonList;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean getCanAllow() {
            return this.displayAllowButton;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean getCanFlag() {
            return this.displayFlagButton;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean getCanClose() {
            return this.displayCloseButton;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean getCanPin() {
            return this.displayPinButton;
        }

        @Override
        public boolean getCanMove() {
            return this.canMove;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean getCanMarkFeatured() {
            return this.displayFeaturedLink;
        }

        /**
         * @return true if the Review action button should be displayed.
         */
        public boolean getCanReview() {
            return this.displayReviewButton;
        }

        /**
         * @return list of the states that moderators can move the resource to.
         */
        public List getReviewStates() {
            return nextStates;
        }

    }

    /**
     * Gets information about the pages for this collection. This can be used by the page block helper to easily
     * render various pagination UIs.
     * @return information about the pagination system
     */
    @Override
    public PageInfo getPageInfo() {
        return this.pageInfo;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getTranslationDescription() {
        return clientUtils.filterHTML(this.descriptionTranslation);
    }

    /**
     * {@inheritDoc}

     */
    @Override
    public String getTranslationAttribution() {
        return this.translationAttribution;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getTranslationTitle() {
        return this.titleTranslation;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getTranslationDisplay() {
        return this.displayTranslation;
    }

    @Override
    public String getFriendlyUrl() {
        String url = getReferrerUrl();
        if (StringUtils.isNotBlank(url)) {
            if (url.contains(CATALOG + RESOURCE)) {
                url = removePageinationFromReferer(url, RESOURCE);
            } else if (url.contains(CATALOG + LEARNINGPATH)) {
                url = removePageinationFromReferer(url, LEARNINGPATH);
            }
            String commentFilter = "";
            if (url.indexOf(PAGE) < 0) {
                String asiComment = url.substring(url.lastIndexOf("/"));
                String[] commentPathArray = getId().getResourceIdentifier().split(asiComment + "/");
                if (commentPathArray.length > 1) {
                    String commentPath = commentPathArray[1];
                    if (commentPath.startsWith(RESOURCE_COMMENTS)) { //Comments on resources has additional strings and we should remove them
                        commentPath = commentPath.replace(RESOURCE_COMMENTS, "");
                    }
                    try {
                        commentFilter = "?" + FILTER + "=" + URLEncoder.encode(PAGE + " ", "UTF-8") + commentPath;
                    } catch (UnsupportedEncodingException ex) {
                        commentFilter = "?" + FILTER + "=" + (PAGE + " ").replace(" ", "%20") + commentPath;
                    }
                }
            }
            return url + commentFilter;
        }
        return super.getFriendlyUrl();
    }

    //Till we find a better of removing pagination from referrer. Ideally, it should be handled in comment creation
    // logic, but for backward compatibility, we need to handle it here
    private String removePageinationFromReferer(final String referer, final String enablementType) {
        String[] urlParts = referer.split("catalog");
        String url = referer;
        if (urlParts.length >= 2) {
            String[] urlPartsNextSplit = urlParts[1].split("\\.");
            if (urlPartsNextSplit.length >= 1) {
                if (!urlPartsNextSplit[1].startsWith("html")) {
                    url = urlParts[0] + "catalog." + enablementType + ".html" + urlParts[1].split(".html")[1];
                }
            }
        }
        return url;
    }

    @Override
    public String getReferrerUrl() {
        return (useReferrerUrl) ? comment.getProperty(SocialComponent.PROP_REFERER, "") : null;
    }
}