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

com.adobe.cq.social.group.client.endpoints.AbstractCommunityGroupOperationService Maven / Gradle / Ivy

/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2013 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.group.client.endpoints;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.activation.DataSource;
import javax.jcr.Binary;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.jackrabbit.JcrConstants;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.jcr.api.SlingRepository;
import org.apache.sling.jcr.resource.JcrResourceConstants;
import org.apache.sling.settings.SlingSettingsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.cq.social.blueprint.api.SiteActivationService;
import com.adobe.cq.social.commons.CollabUtil;
import com.adobe.cq.social.community.api.CommunityConstants;
import com.adobe.cq.social.community.api.CommunityContext;
import com.adobe.cq.social.community.api.CommunityUserGroup;
import com.adobe.cq.social.communityfunctions.api.CommunityFunction;
import com.adobe.cq.social.group.api.GroupConstants;
import com.adobe.cq.social.group.api.GroupException;
import com.adobe.cq.social.group.api.GroupService;
import com.adobe.cq.social.group.api.GroupUtil;
import com.adobe.cq.social.group.bundleactivator.impl.Activator;
import com.adobe.cq.social.group.client.api.CommunityGroup;
import com.adobe.cq.social.group.client.api.CommunityGroupConstants;
import com.adobe.cq.social.group.client.endpoints.impl.CommunityGroupOperationService;
import com.adobe.cq.social.scf.Operation;
import com.adobe.cq.social.scf.OperationException;
import com.adobe.cq.social.scf.OperationExtension;
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.core.operations.AbstractOperationService;
import com.adobe.cq.social.serviceusers.internal.ServiceUserWrapper;
import com.adobe.cq.social.ugcbase.AsyncReverseReplicator;
import com.day.cq.commons.jcr.JcrUtil;
import com.day.cq.replication.ReplicationActionType;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;

/**
 * Provides abstract implementation of community group operations. This class can be extended to implement create
 * operations for any component that extends the community group.
 * @param  is a {@link OperationExtension} that will be used as hooks by the extending class.
 * @param  is a {@link Operation} that is being provided by the extending class.
 */
@Component(metatype = false, componentAbstract = true)
public abstract class AbstractCommunityGroupOperationService
    extends AbstractOperationService implements CommunityGroupOperations {

    private static final String MSM_SERVICE = "msm-service";

    private static final Logger LOG = LoggerFactory.getLogger(CommunityGroupOperationService.class);

    private static final String PROPERTY_IMAGE_NAME = "image";
    private static final int PARAM_NAME_INDEX = 0;
    private static final int PARAM_CLASS_INDEX = 1;
    private static final int PARAM_REQUIRED_INDEX = 2;
    private static final Object requestParams[][] = {
        {CommunityGroupConstants.PROP_COMMUNITY_GROUP_DESCRIPTION, String.class, Boolean.FALSE},
        {CommunityGroupConstants.PROP_COMMUNITY_GROUP_NAME, String.class, Boolean.TRUE},
        {CommunityGroupConstants.PROP_COMMUNITY_GROUP_TITLE, String.class, Boolean.FALSE},
        {CommunityGroupConstants.PROP_COMMUNITY_GROUP_TYPE, String.class, Boolean.FALSE},
        {CommunityGroupConstants.PROP_COMMUNITY_GROUP_INVITE, String.class, Boolean.FALSE},
        {CommunityGroupConstants.PROP_COMMUNITY_GROUP_BLUEPRINT_ID, String.class, Boolean.TRUE}};
    private static final String specialParams[] = {CommunityGroupConstants.PROP_COMMUNITY_GROUP_FILE};

    // TODO: Define this value from configuration
    private static final int ATTACHMENT_FILE_LIMIT = Integer.MAX_VALUE;
    private static final List WHITE_LIST = new ArrayList();
    private static final String[] BLACK_LIST = new String[0];

    private static final String USER_ADMIN = "user-admin";
    private static final String DEFAULT_GROUP_ROOTTEMPLATE_ROOT = "/etc/community/templates/groups";
    private static final String DEFAULT_GROUP_TEMPLATE_ROOT = DEFAULT_GROUP_ROOTTEMPLATE_ROOT
            + CommunityConstants.REFERENCE_SUBPATH;
    private static final String CUSTOM_GROUP_TEMPLATE_ROOT = DEFAULT_GROUP_ROOTTEMPLATE_ROOT
            + CommunityConstants.CUSTOM_SUBPATH;
    private static final String PATH = "path";
    private static final String NAME = "name";

    // delimiters to split the invited id list
    private final String delimiters = "[\\s,;]";

    /** Social Component Factory Manager. */
    @Reference
    private SocialComponentFactoryManager componentFactoryManager;

    @Reference
    private AsyncReverseReplicator replicator;

    @Reference
    private GroupService groupService;

    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY, policy = ReferencePolicy.STATIC)
    private SlingRepository repository;

    @Reference
    private ServiceUserWrapper serviceUserWrapper;

    @Reference
    private ResourceResolverFactory resourceResolverFactory;

    @Reference
    protected SlingSettingsService settingsService;

    /**
     * This is needed to get a ResourceResolverFactory tied to this bundle's service user configurations. The Felix DS
     * implementation will inject a ResourceResolverFactory tied to the bundle of the concrete implementation of this
     * component.
     */
    private ResourceResolverFactory bundleLocalResourceResolverFactory;

    private void cleanupFailure(final Session session) {
        try {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Failure cleanup invoked.", new Throwable());
            }
            session.refresh(false);
        } catch (final RepositoryException e) {
            LOG.info("Failed to refresh the session", e);
        }

    }

    private ResourceResolverFactory getBundleLocalResourceResolverFactory() {
        if (bundleLocalResourceResolverFactory == null) {
            bundleLocalResourceResolverFactory = Activator.getService(ResourceResolverFactory.class);
        }
        return bundleLocalResourceResolverFactory;
    }

    private ServiceUserWrapper getServiceUserWrapper() {
        if (serviceUserWrapper == null) {
            serviceUserWrapper = Activator.getService(ServiceUserWrapper.class);
        }
        return serviceUserWrapper;
    }

    @Override
    public Resource create(final SlingHttpServletRequest request) throws OperationException {
        final Resource resource = request.getResource();
        final ResourceResolver resolver = resource.getResourceResolver();
        final Session session = resource.getResourceResolver().adaptTo(Session.class);

        if (!GroupUtil.isGroupAdmin(session, resource)) {
            throw new OperationException("No permission to create a community group.",
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }

        // Get the other definition/configuration properties of the site
        final Map props = new HashMap();
        try {
            getDefaultProperties(request, props, true);
            getCustomProperties(request, props, session);
        } catch (final RepositoryException e) {
            throw new OperationException("Failed to obtain community group parameters.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }

        // add creator to the invite list
        String invites = (String) props.get(CommunityGroupConstants.PROP_COMMUNITY_GROUP_INVITE);
        if (!StringUtils.isEmpty(invites)) {
            invites += "," + session.getUserID();
        } else {
            invites = session.getUserID();
        }
        props.put(CommunityGroupConstants.PROP_COMMUNITY_GROUP_INVITE, invites);
        props.put(CommunityGroupConstants.PROP_COMMUNITY_GROUP_CREATOR, session.getUserID());

        // Get the name of the new site
        final String name = (String) props.get(CommunityGroupConstants.PROP_COMMUNITY_GROUP_NAME);
        final PageManager pm = resolver.adaptTo(PageManager.class);
        final Page groupPage = pm.getContainingPage(resource);
        final String root = groupPage.getPath();
        final Resource groupPageResource = resolver.getResource(root);
        if (!GroupUtil.validateGroupName(resolver, name, root)) {
            throw new OperationException("Community group name is badly formatted " + name,
                HttpServletResponse.SC_BAD_REQUEST);
        }

        // get the attachment images
        final List attachments =
            getAttachmentsFromRequest(request, CommunityGroupConstants.PROP_COMMUNITY_GROUP_FILE);
        // create the community group
        // need to open a userAdminSession, since we allow every login user to create groups
        ResourceResolver userAdminResolver = null;
        try {
            userAdminResolver =
                getServiceUserWrapper().getServiceResourceResolver(getBundleLocalResourceResolverFactory(),
                    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object) USER_ADMIN));
            return create(groupPageResource, name, props, attachments, userAdminResolver.adaptTo(Session.class));
        } catch (final LoginException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to join community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } finally {
            if (userAdminResolver != null) {
                userAdminResolver.close();
            }
        }
    }

    protected Resource create(final Resource root, final String name, final Map properties,
        final List attachments, final Session session) throws OperationException {
        try {
            final Map authInfo = new HashMap();
            authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session);
            final ResourceResolver resolver = resourceResolverFactory.getResourceResolver(authInfo);
            final U createOperation = getCreateOperation();
            performBeforeActions(createOperation, session, root, properties);

            final String groupPath = root.getPath() + "/" + name;

            final CommunityContext context = root.adaptTo(CommunityContext.class);
            String nuggetRoot = context != null ? context.getSitePayloadPath() : "/var/community/publish";
            nuggetRoot =
                StringUtils.replace(nuggetRoot, CommunityGroupConstants.ROOT_FOR_PUBLISH_COMMUNITY_NUGGETS,
                    CommunityGroupConstants.ROOT_FOR_CREATE_COMMUNITY_NUGGETS);
            final String nuggetPath = nuggetRoot + groupPath;
            final Node folder = JcrUtil.createPath(nuggetPath, "sling:Folder", session);
            final Node nugget =
                folder.addNode(SiteActivationService.REPLICATE_NODE_NAME, SiteActivationService.REPLICATE_NODE_TYPE);
            nugget.setProperty(SiteActivationService.REPLICATE_PROPERTY_PATH, root.getPath());
            nugget.setProperty(SiteActivationService.REPLICATE_PROPERTY_ACTION,
                CommunityGroupConstants.ACTION_TYPE_CREATE_COMMUNITY_GROUP);
            nugget.setProperty(GroupConstants.PROPERTY_FORM_PAYLOAD, nuggetPath);
            for (final Entry property : properties.entrySet()) {
                final String key = property.getKey();
                if (!isSpecialRequestParam(key)) {
                    JcrUtil.setProperty(nugget, key, property.getValue());
                }
            }
            // add image node if one has been uploaded
            final boolean hasImage = !attachments.isEmpty();
            if (hasImage) {
                final DataSource ds = attachments.get(0);
                addImage(nuggetPath, resolver, ds.getInputStream(), ds.getContentType());
            }
            session.save();

            // reverse replicate nugget node
            if (resolver.getResource(nuggetPath) != null && settingsService != null
                    && settingsService.getRunModes().contains("publish")) {
                final List paths = new ArrayList(2);
                paths.add(nuggetPath);
                if (hasImage) {
                    paths.add(nuggetPath + "/" + PROPERTY_IMAGE_NAME);
                }
                replicator.reverseReplicate(ReplicationActionType.ACTIVATE, paths);
            }

            performAfterActions(createOperation, session, null, properties);
            /* Perform the wait with the msm-service as the user admin session we have can't read content. */
            ResourceResolver msmResolver = null;
            try {
                msmResolver =
                    getServiceUserWrapper().getServiceResourceResolver(getBundleLocalResourceResolverFactory(),
                        Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object) MSM_SERVICE));

                GroupUtil.waitForPageCreation(msmResolver, groupPath, 120 * 1000, 15 * 1000);
            } finally {
                if (msmResolver != null) {
                    msmResolver.close();
                }
            }

            return root.getResourceResolver().resolve(groupPath + "/" + CommunityGroupConstants.CONFIG_NODE_NAME);
        } catch (final OperationException e) {
            cleanupFailure(session);
            throw e;
        } catch (final Exception e) {
            cleanupFailure(session);
            throw new OperationException("Failed to create community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }

    }

    @Override
    public boolean approveJoin(final ResourceResolver resolver, final CommunityGroup group) throws OperationException {
        // anonymous cannot join
        final Session session = resolver.adaptTo(Session.class);
        if (StringUtils.endsWithIgnoreCase(session.getUserID(), "anonymous")) {
            return false;
        }
        // auto approve the request to join open community groups
        if (group != null && GroupConstants.TYPE_OPEN.equals(group.getType())) {
            return true;
        }
        // currently reject all other requests
        return false;
    }

    @Override
    public Resource join(final SlingHttpServletRequest request) throws OperationException {
        final Resource resource = request.getResource();
        final ResourceResolver resolver = resource.getResourceResolver();
        final Session session = resource.getResourceResolver().adaptTo(Session.class);
        final String groupUrl = request.getParameter(CommunityGroupConstants.PROP_COMMUNITY_GROUP_PATH);
        if (StringUtils.isBlank(groupUrl)) {
            return null;
        }
        final Resource groupPageResource = resolver.resolve(groupUrl);
        final CommunityGroup group =
            (CommunityGroup) this.getCommunityGroupComponentForResource(groupPageResource, request);
        return join(resolver, group, session);
    }

    protected Resource join(final ResourceResolver resolver, final CommunityGroup group, final Session session)
        throws OperationException {
        if (resolver == null || group == null || session == null) {
            return null;
        }

        if (!approveJoin(resolver, group)) {
            throw new OperationException("Deny " + session.getUserID() + "'s request to join community group "
                    + group.getName(), HttpServletResponse.SC_NOT_ACCEPTABLE);
        }

        ResourceResolver userAdminResolver = null;
        try {
            final String authorizableId = session.getUserID();
            userAdminResolver =
                getServiceUserWrapper().getServiceResourceResolver(getBundleLocalResourceResolverFactory(),
                    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object) USER_ADMIN));
            groupService.addGroupMember(userAdminResolver, group.getMemberGroupId(), authorizableId);
            resolver.adaptTo(Session.class).refresh(false);
            resolver.refresh();
        } catch (final LoginException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to login as user admin.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final GroupException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to join community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final RepositoryException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to join community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } finally {
            if (userAdminResolver != null) {
                userAdminResolver.close();
            }
        }
        return group.getResource();
    }

    @Override
    public Resource leave(final SlingHttpServletRequest request) throws OperationException {
        final Resource resource = request.getResource();
        final ResourceResolver resolver = resource.getResourceResolver();
        final Session session = resource.getResourceResolver().adaptTo(Session.class);
        final String groupUrl = request.getParameter(CommunityGroupConstants.PROP_COMMUNITY_GROUP_PATH);
        if (StringUtils.isBlank(groupUrl)) {
            return null;
        }
        final Resource groupPageResource = resolver.resolve(groupUrl);
        final CommunityGroup group =
            (CommunityGroup) this.getCommunityGroupComponentForResource(groupPageResource, request);
        return leave(resolver, group, session);
    }

    protected Resource leave(final ResourceResolver resolver, final CommunityGroup group, final Session session)
        throws OperationException {
        if (resolver == null || group == null || session == null) {
            return null;
        }

        ResourceResolver userAdminResolver = null;
        try {
            final String authorizableId = session.getUserID();
            userAdminResolver =
                getServiceUserWrapper().getServiceResourceResolver(getBundleLocalResourceResolverFactory(),
                    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object) USER_ADMIN));
            groupService.removeGroupMember(userAdminResolver, group.getMemberGroupId(), authorizableId);
            resolver.adaptTo(Session.class).refresh(false);
            resolver.refresh();
        } catch (final LoginException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to login as user admin.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final GroupException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to join community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final RepositoryException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to join community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } finally {
            if (userAdminResolver != null) {
                userAdminResolver.close();
            }
        }
        return group.getResource();
    }

    @Override
    public Resource invite(final SlingHttpServletRequest request) throws OperationException {
        final Resource resource = request.getResource();
        final ResourceResolver resolver = resource.getResourceResolver();
        final Session session = resource.getResourceResolver().adaptTo(Session.class);
        final String[] inviteList = request.getParameterValues(CommunityGroupConstants.PROP_COMMUNITY_GROUP_USERS);
        final CommunityContext context = resource.adaptTo(CommunityContext.class);
        final Resource groupPageResource = resolver.resolve(context.getCommunityGroupPath());
        final CommunityGroup group =
            (CommunityGroup) this.getCommunityGroupComponentForResource(groupPageResource, request);
        invite(resolver, group, inviteList, context, session);
        return resource;
    }

    protected void invite(final ResourceResolver resolver, final CommunityGroup group, final String[] inviteList,
        final CommunityContext context, final Session session) throws OperationException {
        if (resolver == null || group == null || session == null) {
            return;
        }

        if (!GroupUtil.canInviteGroupMember(resolver, context)) {
            throw new OperationException("Deny " + session.getUserID()
                    + "'s request to invite users to community group " + group.getName(),
                HttpServletResponse.SC_NOT_ACCEPTABLE);
        }

        ResourceResolver userAdminResolver = null;
        try {
            userAdminResolver =
                getServiceUserWrapper().getServiceResourceResolver(getBundleLocalResourceResolverFactory(),
                    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object) USER_ADMIN));
            groupService.addGroupMembers(userAdminResolver, group.getMemberGroupId(), inviteList);
            session.refresh(false);
            resolver.refresh();
        } catch (final LoginException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to login as user admin.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final GroupException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to join community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final RepositoryException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to join community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } finally {
            if (userAdminResolver != null) {
                userAdminResolver.close();
            }
        }
    }

    @Override
    public Resource uninvite(final SlingHttpServletRequest request) throws OperationException {
        final Resource resource = request.getResource();
        final ResourceResolver resolver = resource.getResourceResolver();
        final Session session = resource.getResourceResolver().adaptTo(Session.class);
        final String invite = request.getParameter(CommunityGroupConstants.PROP_COMMUNITY_GROUP_USERS);
        if (StringUtils.isEmpty(invite)) {
            return null;
        }
        final String[] inviteList = invite.split(delimiters);
        final CommunityContext context = resource.adaptTo(CommunityContext.class);
        final Resource groupPageResource = resolver.resolve(context.getCommunityGroupPath());
        final CommunityGroup group =
            (CommunityGroup) this.getCommunityGroupComponentForResource(groupPageResource, request);
        uninvite(resolver, group, inviteList, context, session);
        return resource;
    }

    protected void uninvite(final ResourceResolver resolver, final CommunityGroup group, final String[] inviteList,
        final CommunityContext context, final Session session) throws OperationException {
        if (resolver == null || group == null || session == null) {
            return;
        }

        if (!GroupUtil.canInviteGroupMember(resolver, context)) {
            throw new OperationException("Deny " + session.getUserID()
                    + "'s request to invite users to community group " + group.getName(),
                HttpServletResponse.SC_NOT_ACCEPTABLE);
        }

        ResourceResolver userAdminResolver = null;
        try {
            userAdminResolver =
                getServiceUserWrapper().getServiceResourceResolver(getBundleLocalResourceResolverFactory(),
                    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object) USER_ADMIN));
            groupService.removeGroupMembers(userAdminResolver, group.getMemberGroupId(), inviteList);
            session.refresh(false);
            resolver.refresh();
        } catch (final LoginException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to login as user admin.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final GroupException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to uninvite member from community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final RepositoryException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to uninvite member from community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } finally {
            if (userAdminResolver != null) {
                userAdminResolver.close();
            }
        }
    }

    @Override
    public Resource promoteMember(final SlingHttpServletRequest request) throws OperationException {
        final Resource resource = request.getResource();
        final ResourceResolver resolver = resource.getResourceResolver();
        final Session session = resource.getResourceResolver().adaptTo(Session.class);
        final String[] inviteList = request.getParameterValues(CommunityGroupConstants.PROP_COMMUNITY_GROUP_USERS);
        final CommunityContext context = resource.adaptTo(CommunityContext.class);
        final Resource groupPageResource = resolver.resolve(context.getCommunityGroupPath());
        final CommunityGroup group =
            (CommunityGroup) this.getCommunityGroupComponentForResource(groupPageResource, request);
        LOG.debug("promoteMember: resource {} groupPageResource {}", resource, groupPageResource);
        LOG.debug("promoteMember: communityGroupPath {}", context.getCommunityGroupPath());
        promoteMember(resolver, group, context, inviteList, session);
        return resource;
    }

    protected void promoteMember(final ResourceResolver resolver, final CommunityGroup group,
        final CommunityContext context, final String[] inviteList, final Session session) throws OperationException {
        final String groupAdminID = context.getSiteUserGroupName(CommunityUserGroup.GROUP_ADMIN);
        if (LOG.isDebugEnabled()) {
            LOG.debug("promoteMember: {} to {}", Arrays.asList(inviteList), groupAdminID);
        }
        if (resolver == null) {
            throw new IllegalArgumentException("resolver not allowed to be null");
        }
        if (group == null) {
            throw new IllegalArgumentException("group not allowed to be null");
        }
        if (session == null) {
            throw new IllegalArgumentException("session not allowed to be null");
        }

        if (!GroupUtil.canPromoteGroupMember(resolver, context)) {
            throw new OperationException("Deny " + session.getUserID()
                    + "'s request to invite users to community group " + group.getName(),
                HttpServletResponse.SC_NOT_ACCEPTABLE);
        }

        LOG.debug("promoteMember: {} is authorized.", resolver.getUserID());

        ResourceResolver userAdminResolver = null;
        try {
            userAdminResolver =
                getServiceUserWrapper().getServiceResourceResolver(getBundleLocalResourceResolverFactory(),
                    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object) USER_ADMIN));
            LOG.debug("promoteMember: calling addGroupMembers adding {} to {}.", Arrays.asList(inviteList),
                groupAdminID);
            groupService.addGroupMembers(userAdminResolver, groupAdminID, inviteList);
            session.refresh(false);
            resolver.refresh();
        } catch (final LoginException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to login as user admin.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final GroupException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to join community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final RepositoryException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to join community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } finally {
            if (userAdminResolver != null) {
                userAdminResolver.close();
            }
        }
    }

    @Override
    public Resource demoteMember(final SlingHttpServletRequest request) throws OperationException {
        final Resource resource = request.getResource();
        final ResourceResolver resolver = resource.getResourceResolver();
        final Session session = resource.getResourceResolver().adaptTo(Session.class);
        final String[] inviteList = request.getParameterValues(CommunityGroupConstants.PROP_COMMUNITY_GROUP_USERS);
        final CommunityContext context = resource.adaptTo(CommunityContext.class);
        final Resource groupPageResource = resolver.resolve(context.getCommunityGroupPath());
        final CommunityGroup group =
            (CommunityGroup) this.getCommunityGroupComponentForResource(groupPageResource, request);
        demoteMember(resolver, group, context, inviteList, session);
        return resource;
    }

    protected void demoteMember(final ResourceResolver resolver, final CommunityGroup group,
        final CommunityContext context, final String[] inviteList, final Session session) throws OperationException {
        if (resolver == null || group == null || session == null) {
            return;
        }

        final String groupAdminID = context.getSiteUserGroupName(CommunityUserGroup.GROUP_ADMIN);
        if (!GroupUtil.canPromoteGroupMember(resolver, context)) {
            throw new OperationException("Deny " + session.getUserID()
                    + "'s request to invite users to community group " + group.getName(),
                HttpServletResponse.SC_NOT_ACCEPTABLE);
        }

        ResourceResolver userAdminResolver = null;
        try {
            userAdminResolver =
                getServiceUserWrapper().getServiceResourceResolver(getBundleLocalResourceResolverFactory(),
                    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object) USER_ADMIN));
            groupService.removeGroupMembers(userAdminResolver, groupAdminID, inviteList);
            groupService.addGroupMembers(userAdminResolver, group.getMemberGroupId(), inviteList);
            session.refresh(false);
            resolver.refresh();
        } catch (final LoginException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to login as user admin.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final GroupException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to join community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final RepositoryException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to join community group.", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } finally {
            if (userAdminResolver != null) {
                userAdminResolver.close();
            }
        }
    }

    private boolean isSpecialRequestParam(final String paramName) {
        for (int i = 0; i < specialParams.length; i++) {
            if (paramName.equals(specialParams[i])) {
                return true;
            }
        }
        return false;
    }

    protected void addImage(final String path, final ResourceResolver resolver, final InputStream imageStream,
        final String contentType) throws OperationException, RepositoryException {
        try {
            if (imageStream != null && imageStream.available() > 0) {
                final Resource folder =
                    ResourceUtil.getOrCreateResource(resolver, path, CommunityGroupConstants.FOLDER_NODETYPE, null,
                        true);
                final Node folderNode = folder.adaptTo(Node.class);
                final Node imageNode = folderNode.addNode(PROPERTY_IMAGE_NAME, JcrConstants.NT_FILE);
                final Node imageContentNode = imageNode.addNode(JcrConstants.JCR_CONTENT, JcrConstants.NT_RESOURCE);
                final Binary data = resolver.adaptTo(Session.class).getValueFactory().createBinary(imageStream);
                imageContentNode.setProperty(JcrConstants.JCR_MIMETYPE, contentType);
                imageContentNode.setProperty(JcrConstants.JCR_DATA, data);
                imageContentNode.setProperty(JcrConstants.JCR_LASTMODIFIED, Calendar.getInstance());
            }
        } catch (final IOException e) {
            throw new OperationException("IO failure", e, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }

        // NOTE: caller should save the new image node
    }

    protected List getAttachmentsFromRequest(final SlingHttpServletRequest request,
        final String requestParameterName) {
        final RequestParameter[] fileRequestParameters = request.getRequestParameters(requestParameterName);
        if (fileRequestParameters != null) {
            // Didn't find equivalent in SocialUitl
            return CollabUtil.getAttachmentsFromRequest(fileRequestParameters, ATTACHMENT_FILE_LIMIT, WHITE_LIST,
                BLACK_LIST);
        }
        return Collections.emptyList();
    }

    /**
     * Extract the default site properties from the specified {@link SlingHttpServletRequest} into the specified.
     * {@link Map}
     * @param request
     * @param props
     * @param session
     * @throws RepositoryException
     * @throws OperationException
     */
    protected void getDefaultProperties(final SlingHttpServletRequest request, final Map props,
        final boolean validateRequired) throws RepositoryException, OperationException {

        for (int i = 0; i < requestParams.length; i++) {
            final Object params[] = requestParams[i];
            final Class clazz = (Class) params[PARAM_CLASS_INDEX];
            final String name = (String) params[PARAM_NAME_INDEX];
            if (clazz.isArray()) {
                final String values[] = request.getParameterValues(name);
                if (validateRequired && values == null && ((Boolean) params[PARAM_REQUIRED_INDEX])) {
                    throw new OperationException("Community group value '" + name + "' is empty",
                        HttpServletResponse.SC_BAD_REQUEST);

                }
                if (clazz == String[].class) {
                    if (values != null) {
                        props.put(name, values);
                    }
                }
                // TODO: Handle other array types
            } else {
                final String value = request.getParameter(name);
                if (validateRequired && value == null && ((Boolean) params[PARAM_REQUIRED_INDEX])) {
                    throw new OperationException("Community group value '" + name + "' is empty",
                        HttpServletResponse.SC_BAD_REQUEST);
                }
                if (value != null) {
                    props.put(name, GroupUtil.toObject(value, clazz));
                }
            }
        }

    }

    /**
     * Extract the custom site properties from the specified {@link SlingHttpServletRequest} into the specified.
     * {@link Map}
     * @param request
     * @param props
     * @param session
     * @throws RepositoryException
     * @throws OperationException
     */
    protected void getCustomProperties(final SlingHttpServletRequest request, final Map props,
        final Session session) throws RepositoryException, OperationException {
        // TODO
    }

    private void addAllowedTemplate(final String path, final List allowedTemplates,
        final ResourceResolver resolver) {
        final Resource res = resolver.resolve(path);
        addAllowedTemplate(res, allowedTemplates, resolver);
    }

    private void addAllowedTemplate(final Resource res, final List allowedTemplates,
        final ResourceResolver resolver) {
        if (res != null && !ResourceUtil.isNonExistingResource(res)) {
            final ValueMap v = res.adaptTo(ValueMap.class);
            final boolean isDisabled = v.get(CommunityFunction.PROPERTY_BLUEPRINT_ENABLED, false);
            if (!isDisabled) {
                final Map data = new HashMap();
                data.put(PATH, res.getPath());
                data.put(NAME, v.get("jcr:title"));
                allowedTemplates.add(data);
            }
        }
    }

    private void addTemplatesUnderResource(final String path, final List allowedTemplates,
        final ResourceResolver userAdminResolver) {
        final Resource templates = userAdminResolver.resolve(path);
        if (templates != null && templates.hasChildren()) {
            for (final Resource res : templates.getChildren()) {
                if (res != null && !ResourceUtil.isNonExistingResource(res)) {
                    final ValueMap v = res.adaptTo(ValueMap.class);
                    final Map data = new HashMap();
                    data.put(PATH, res.getPath());
                    data.put(NAME, v.get("jcr:title"));
                    allowedTemplates.add(data);
                }
            }
        }
    }

    @Override
    public List getAllowedTemplateForEveryone(final String[] paths) {
        final List allowedTemplates = new ArrayList();
        ResourceResolver msmResolver = null;
        try {
            msmResolver =
                getServiceUserWrapper().getServiceResourceResolver(getBundleLocalResourceResolverFactory(),
                    Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, (Object) MSM_SERVICE));
            if (paths != null && paths.length > 0) {
                for (int i = 0; i < paths.length; i++) {
                    addAllowedTemplate(paths[i], allowedTemplates, msmResolver);
                }
            } else {
                addTemplatesUnderResource(DEFAULT_GROUP_TEMPLATE_ROOT, allowedTemplates, msmResolver);
                addTemplatesUnderResource(CUSTOM_GROUP_TEMPLATE_ROOT, allowedTemplates, msmResolver);
            }
        } catch (final LoginException e) {
            LOG.error("Failed to login as user admin.", e);
        } finally {
            if (msmResolver != null) {
                msmResolver.close();
            }
        }
        return allowedTemplates;
    }

    /**
     * Get the SocialComponent for the specified {@link Resource} and {@link SlingHttpServletRequest}.
     * @param communityGroup the target community group
     * @param request the client request
     * @return the {@link SocialComponent}
     */
    @Override
    public SocialComponent getCommunityGroupComponentForResource(final Resource communityGroup,
        final SlingHttpServletRequest request) {
        String path = communityGroup.getPath();
        if (!path.endsWith(CommunityGroupConstants.CONFIG_NODE_NAME)) {
            path = path + "/" + CommunityGroupConstants.CONFIG_NODE_NAME;
        }
        final Resource resource = request.getResourceResolver().getResource(path);
        if (resource == null) {
            LOG.debug("getCommunityGroupComponentForResource: Resource at {} not found with resolver for user {}",
                path, request.getResourceResolver().getUserID());
            return null;
        }
        final SocialComponentFactory factory = componentFactoryManager.getSocialComponentFactory(resource);
        if (factory == null) {
            LOG.debug("getCommunityGroupComponentForResource: SocialComponentFactory responsible for {} not found.",
                resource.getPath());
            return null;
        }
        final SocialComponent sc = factory.getSocialComponent(resource, request);
        if (sc == null) {
            LOG.debug("getCommunityGroupComponentForResource: SocialComponentFactory {} returned null for {}.", sc,
                resource.getPath());
        }
        return sc;
    }

    /**
     * Get the SocialComponent for the specified {@link Resource} and {@link SlingHttpServletRequest}.
     * @param communityMembers the target community member list
     * @param request the client request
     * @return the {@link SocialComponent}
     */
    @Override
    public SocialComponent getCommunityMemberListComponentForResource(final Resource communityMembers,
        final SlingHttpServletRequest request) {
        final String path = communityMembers.getPath();
        final Resource resource = request.getResourceResolver().getResource(path);
        if (resource == null) {
            return null;
        }
        final SocialComponentFactory factory = componentFactoryManager.getSocialComponentFactory(resource);
        return (factory != null) ? factory.getSocialComponent(resource, request) : null;
    }

    protected abstract U getCreateOperation();

    protected abstract U getJoinOperation();

    protected abstract U getLeaveOperation();

}